1. 1 : /**
  2. 2 : * Roundcube Webmail Client Script
  3. 3 : *
  4. 4 : * This file is part of the Roundcube Webmail client
  5. 5 : *
  6. 6 : * @licstart The following is the entire license notice for the
  7. 7 : * JavaScript code in this file.
  8. 8 : *
  9. 9 : * Copyright (C) The Roundcube Dev Team
  10. 10 : * Copyright (C) Kolab Systems AG
  11. 11 : *
  12. 12 : * The JavaScript code in this page is free software: you can
  13. 13 : * redistribute it and/or modify it under the terms of the GNU
  14. 14 : * General Public License (GNU GPL) as published by the Free Software
  15. 15 : * Foundation, either version 3 of the License, or (at your option)
  16. 16 : * any later version. The code is distributed WITHOUT ANY WARRANTY;
  17. 17 : * without even the implied warranty of MERCHANTABILITY or FITNESS
  18. 18 : * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
  19. 19 : *
  20. 20 : * As additional permission under GNU GPL version 3 section 7, you
  21. 21 : * may distribute non-source (e.g., minimized or compacted) forms of
  22. 22 : * that code without the copy of the GNU GPL normally required by
  23. 23 : * section 4, provided you include this license notice and a URL
  24. 24 : * through which recipients can access the Corresponding Source.
  25. 25 : *
  26. 26 : * @licend The above is the entire license notice
  27. 27 : * for the JavaScript code in this file.
  28. 28 : *
  29. 29 : * @author Thomas Bruederli <roundcube@gmail.com>
  30. 30 : * @author Aleksander 'A.L.E.C' Machniak <alec@alec.pl>
  31. 31 : * @author Charles McNulty <charles@charlesmcnulty.com>
  32. 32 : *
  33. 33 : * @requires jquery.js, common.js, list.js
  34. 34 : */
  35. 35 :
  36. 36 : function rcube_webmail()
  37. 37 : {
  38. 38 : this.labels = {};
  39. 39 : this.buttons = {};
  40. 40 : this.buttons_sel = {};
  41. 41 : this.gui_objects = {};
  42. 42 : this.gui_containers = {};
  43. 43 : this.commands = {};
  44. 44 : this.command_handlers = {};
  45. 45 : this.onloads = [];
  46. 46 : this.messages = {};
  47. 47 : this.group2expand = {};
  48. 48 : this.http_request_jobs = {};
  49. 49 : this.menu_stack = [];
  50. 50 : this.menu_buttons = {};
  51. 51 : this.entity_selectors = [];
  52. 52 : this.image_style = {};
  53. 53 : this.uploads = {};
  54. 54 :
  55. 55 : // webmail client settings
  56. 56 : this.dblclick_time = 500;
  57. 57 : this.message_time = 5000;
  58. 58 : this.preview_delay_select = 400;
  59. 59 : this.preview_delay_click = 60;
  60. 60 : this.identifier_expr = /[^0-9a-z_-]/gi;
  61. 61 : this.uploadTimeout = 0; // 0 = no timeout | ajax call timeout for loading attachment
  62. 62 :
  63. 63 :
  64. 64 : // environment defaults
  65. 65 : this.env = {
  66. 66 : attachments: {},
  67. 67 : request_timeout: 180, // seconds
  68. 68 : draft_autosave: 0, // seconds
  69. 69 : comm_path: './',
  70. 70 : recipients_separator: ',', // @deprecated
  71. 71 : recipients_delimiter: ', ', // @deprecated
  72. 72 : popup_width: 1150,
  73. 73 : popup_width_small: 900,
  74. 74 : thread_padding: '15px'
  75. 75 : };
  76. 76 :
  77. 77 : // create protected reference to myself
  78. 78 : this.ref = 'rcmail';
  79. 79 : var ref = this;
  80. 80 :
  81. 81 : // set jQuery ajax options
  82. 82 : $.ajaxSetup({
  83. 83 : cache: false,
  84. 84 : timeout: this.env.request_timeout * 1000,
  85. 85 : error: function(request, status, err){ ref.http_error(request, status, err); },
  86. 86 : beforeSend: function(xmlhttp){
  87. 87 : //PAMELA
  88. 88 : if (window.disable_x_roundcube === true) return;
  89. 89 : else xmlhttp.setRequestHeader('X-Roundcube-Request', ref.env.request_token);
  90. 90 : }
  91. 91 :
  92. 92 : });
  93. 93 :
  94. 94 : // unload fix
  95. 95 : $(window).on('beforeunload', function() { ref.unload = true; });
  96. 96 :
  97. 97 : // set environment variable(s)
  98. 98 : this.set_env = function(p, value)
  99. 99 : {
  100. 100 : if (p != null && typeof p === 'object' && !value)
  101. 101 : for (var n in p)
  102. 102 : this.env[n] = p[n];
  103. 103 : else
  104. 104 : this.env[p] = value;
  105. 105 : };
  106. 106 :
  107. 107 : // add a localized label to the client environment
  108. 108 : this.add_label = function(p, value)
  109. 109 : {
  110. 110 : if (typeof p == 'string')
  111. 111 : this.labels[p] = value;
  112. 112 : else if (typeof p == 'object')
  113. 113 : $.extend(this.labels, p);
  114. 114 : };
  115. 115 :
  116. 116 : // add a button to the button list
  117. 117 : this.register_button = function(command, id, type, act, sel, over)
  118. 118 : {
  119. 119 : var button_prop = {id:id, type:type};
  120. 120 :
  121. 121 : if (act) button_prop.act = act;
  122. 122 : if (sel) button_prop.sel = sel;
  123. 123 : if (over) button_prop.over = over;
  124. 124 :
  125. 125 : if (!this.buttons[command])
  126. 126 : this.buttons[command] = [];
  127. 127 :
  128. 128 : this.buttons[command].push(button_prop);
  129. 129 :
  130. 130 : if (this.loaded) {
  131. 131 : this.init_button(command, button_prop);
  132. 132 : this.set_button(command, (this.commands[command] ? 'act' : 'pas'));
  133. 133 : }
  134. 134 : };
  135. 135 :
  136. 136 : // register a button with popup menu, to set its state according to the state of all commands in the menu
  137. 137 : this.register_menu_button = function(button, menu_id)
  138. 138 : {
  139. 139 : if (this.menu_buttons[menu_id]) {
  140. 140 : this.menu_buttons[menu_id][0].push(button);
  141. 141 : }
  142. 142 : else {
  143. 143 : var commands = [];
  144. 144 : $('#' + menu_id).find('a').each(function() {
  145. 145 : var command, link = $(this), onclick = link.attr('onclick');
  146. 146 :
  147. 147 : if (onclick && String(onclick).match(/rcmail\.command\(\'([^']+)/))
  148. 148 : command = RegExp.$1;
  149. 149 : else
  150. 150 : command = function() { return link.is('.active'); };
  151. 151 :
  152. 152 : commands.push(command);
  153. 153 : });
  154. 154 :
  155. 155 : if (commands.length)
  156. 156 : this.menu_buttons[menu_id] = [[button], commands];
  157. 157 : }
  158. 158 :
  159. 159 : this.set_menu_buttons();
  160. 160 : };
  161. 161 :
  162. 162 : // set state of a menu button according to state of all menu actions
  163. 163 : this.set_menu_buttons = function()
  164. 164 : {
  165. 165 : // Use timeouts to not block and set menu button states only once
  166. 166 : clearTimeout(this.menu_buttons_timeout);
  167. 167 : this.menu_buttons_timeout = setTimeout(function() {
  168. 168 : $.each(ref.menu_buttons, function() {
  169. 169 : var disabled = true;
  170. 170 : $.each(this[1], function() {
  171. 171 : var is_func = typeof(this) == 'function';
  172. 172 : if ((is_func && this()) || (!is_func && ref.commands[this])) {
  173. 173 : return disabled = false;
  174. 174 : }
  175. 175 : });
  176. 176 :
  177. 177 : $(this[0]).add($(this[0]).parent('.dropbutton'))
  178. 178 : .addClass(disabled ? 'disabled' : 'active')
  179. 179 : .removeClass(disabled ? 'active' : 'disabled');
  180. 180 : });
  181. 181 : }, 50);
  182. 182 : };
  183. 183 :
  184. 184 : // register a specific gui object
  185. 185 : this.gui_object = function(name, id)
  186. 186 : {
  187. 187 : this.gui_objects[name] = this.loaded ? rcube_find_object(id) : id;
  188. 188 : };
  189. 189 :
  190. 190 : // register a container object
  191. 191 : this.gui_container = function(name, id)
  192. 192 : {
  193. 193 : this.gui_containers[name] = id;
  194. 194 : };
  195. 195 :
  196. 196 : // add a GUI element (html node) to a specified container
  197. 197 : this.add_element = function(elm, container)
  198. 198 : {
  199. 199 : if (this.gui_containers[container] && this.gui_containers[container].jquery)
  200. 200 : this.gui_containers[container].append(elm);
  201. 201 : };
  202. 202 :
  203. 203 : // register an external handler for a certain command
  204. 204 : this.register_command = function(command, callback, enable)
  205. 205 : {
  206. 206 : this.command_handlers[command] = callback;
  207. 207 :
  208. 208 : if (enable)
  209. 209 : this.enable_command(command, true);
  210. 210 : };
  211. 211 :
  212. 212 : // execute the given script on load
  213. 213 : this.add_onload = function(f)
  214. 214 : {
  215. 215 : this.onloads.push(f);
  216. 216 : };
  217. 217 :
  218. 218 : // initialize webmail client
  219. 219 : this.init = function()
  220. 220 : {
  221. 221 : var n;
  222. 222 : this.task = this.env.task;
  223. 223 :
  224. 224 : if (!this.env.blankpage)
  225. 225 : this.env.blankpage = 'about:blank';
  226. 226 :
  227. 227 : // find all registered gui containers
  228. 228 : for (n in this.gui_containers)
  229. 229 : this.gui_containers[n] = $('#'+this.gui_containers[n]);
  230. 230 :
  231. 231 : // find all registered gui objects
  232. 232 : for (n in this.gui_objects)
  233. 233 : this.gui_objects[n] = rcube_find_object(this.gui_objects[n]);
  234. 234 :
  235. 235 : // init registered buttons
  236. 236 : this.init_buttons();
  237. 237 :
  238. 238 : // tell parent window that this frame is loaded
  239. 239 : if (this.is_framed()) {
  240. 240 : parent.rcmail.unlock_frame();
  241. 241 : }
  242. 242 :
  243. 243 : // enable general commands
  244. 244 : this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref',
  245. 245 : 'compose', 'undo', 'about', 'switch-task', 'menu-open', 'menu-close', 'menu-save', true);
  246. 246 :
  247. 247 : // set active task button
  248. 248 : this.set_button(this.task, 'sel');
  249. 249 :
  250. 250 : if (this.env.permaurl)
  251. 251 : this.enable_command('permaurl', 'extwin', true);
  252. 252 :
  253. 253 : switch (this.task) {
  254. 254 :
  255. 255 : case 'mail':
  256. 256 : // enable mail commands
  257. 257 : this.enable_command('list', 'checkmail', 'add-contact', 'search', 'reset-search', 'collapse-folder', 'import-messages', true);
  258. 258 :
  259. 259 : if (this.gui_objects.messagelist) {
  260. 260 : // setup message list cols
  261. 261 : this.msglist_setup(this.env.layout);
  262. 262 :
  263. 263 : this.env.widescreen_list_template = [
  264. 264 : {className: 'threads', cells: ['threads']},
  265. 265 : // PAMELA - Ajout de la priorité dans l'affichage des mails
  266. 266 : {className: 'subject', cells: ['fromto', 'date', 'size', 'status', 'subject', 'priority']},
  267. 267 : {className: 'flags', cells: ['flag', 'attachment']}
  268. 268 : ];
  269. 269 :
  270. 270 : this.message_list = new rcube_list_widget(this.gui_objects.messagelist, {
  271. 271 : multiselect:true, multiexpand:true, draggable:true, keyboard:true,
  272. 272 : column_movable:this.env.col_movable, dblclick_time:this.dblclick_time
  273. 273 : });
  274. 274 : this.message_list
  275. 275 : .addEventListener('initrow', function(o) { ref.init_message_row(o); })
  276. 276 : .addEventListener('dblclick', function(o) { ref.msglist_dbl_click(o); })
  277. 277 : .addEventListener('keypress', function(o) { ref.msglist_keypress(o); })
  278. 278 : .addEventListener('select', function(o) { ref.msglist_select(o); })
  279. 279 : .addEventListener('dragstart', function(o) { ref.drag_start(o); })
  280. 280 : .addEventListener('dragmove', function(e) { ref.drag_move(e); })
  281. 281 : .addEventListener('dragend', function(e) { ref.drag_end(e); })
  282. 282 : .addEventListener('expandcollapse', function(o) { ref.msglist_expand(o); })
  283. 283 : .addEventListener('column_replace', function(o) { ref.msglist_set_coltypes(o); })
  284. 284 : .init();
  285. 285 :
  286. 286 : //Pamela - Rendre le drag'n'drop possible pour les messages dans une frame.
  287. 287 : if (parent !== window)
  288. 288 : {
  289. 289 : setTimeout(() => {
  290. 290 : this.message_list.draggable = true;
  291. 291 : }, 111);
  292. 292 : }
  293. 293 :
  294. 294 : // TODO: this should go into the list-widget code
  295. 295 : $(this.message_list.thead).on('click', 'a.sortcol', function(e){
  296. 296 : return ref.command('sort', $(this).attr('rel'), this);
  297. 297 : });
  298. 298 :
  299. 299 : this.enable_command('toggle_status', 'toggle_flag', 'sort', true);
  300. 300 : this.enable_command('set-listmode', this.env.threads && !this.is_multifolder_listing());
  301. 301 :
  302. 302 : // load messages
  303. 303 : var searchfilter = $(this.gui_objects.search_filter).val();
  304. 304 : if (searchfilter && searchfilter != 'ALL')
  305. 305 : this.filter_mailbox(searchfilter);
  306. 306 : else
  307. 307 : this.command('list');
  308. 308 :
  309. 309 : $(this.gui_objects.qsearchbox).val(this.env.search_text).focusin(function() { ref.message_list.blur(); });
  310. 310 : }
  311. 311 :
  312. 312 : this.set_button_titles();
  313. 313 :
  314. 314 : this.env.message_commands = ['show', 'reply', 'reply-all', 'reply-list',
  315. 315 : 'move', 'copy', 'delete', 'open', 'mark', 'edit', 'viewsource', 'bounce',
  316. 316 : 'print', 'load-attachment', 'download-attachment', 'show-headers', 'hide-headers', 'download',
  317. 317 : 'forward', 'forward-inline', 'forward-attachment', 'change-format'];
  318. 318 :
  319. 319 : if (this.env.action == 'show' || this.env.action == 'preview') {
  320. 320 : this.enable_command(this.env.message_commands, this.env.uid);
  321. 321 : this.enable_command('reply-list', this.env.list_post);
  322. 322 :
  323. 323 : if (this.env.action == 'show') {
  324. 324 : this.http_request('pagenav', {_uid: this.env.uid, _mbox: this.env.mailbox, _search: this.env.search_request},
  325. 325 : this.display_message('', 'loading'));
  326. 326 : }
  327. 327 :
  328. 328 : if (this.env.mail_read_time > 0)
  329. 329 : setTimeout(function() {
  330. 330 : ref.http_post('mark', {_uid: ref.env.uid, _flag: 'read', _mbox: ref.env.mailbox, _quiet: 1});
  331. 331 : }, this.env.mail_read_time * 1000);
  332. 332 :
  333. 333 : if (this.env.blockedobjects) {
  334. 334 : $(this.gui_objects.remoteobjectsmsg).show();
  335. 335 : this.enable_command('load-remote', true);
  336. 336 : }
  337. 337 :
  338. 338 : // make preview/message frame visible
  339. 339 : if (this.env.action == 'preview' && this.is_framed()) {
  340. 340 : this.enable_command('compose', 'add-contact', false);
  341. 341 : parent.rcmail.show_contentframe(true);
  342. 342 : }
  343. 343 :
  344. 344 : if ($.inArray('flagged', this.env.message_flags) >= 0) {
  345. 345 : $(document.body).addClass('status-flagged');
  346. 346 : }
  347. 347 :
  348. 348 : // initialize drag-n-drop on attachments, so they can e.g.
  349. 349 : // be dropped into mail compose attachments in another window
  350. 350 : if (this.gui_objects.attachments)
  351. 351 : $('li > a', this.gui_objects.attachments).not('.drop').on('dragstart', function(e) {
  352. 352 : var n, href = this.href, dt = e.originalEvent.dataTransfer;
  353. 353 : if (dt) {
  354. 354 : // inject username to the uri
  355. 355 : href = href.replace(/^https?:\/\//, function(m) { return m + urlencode(ref.env.username) + '@'});
  356. 356 : // cleanup the node to get filename without the size test
  357. 357 : n = $(this).clone();
  358. 358 : n.children().remove();
  359. 359 :
  360. 360 : dt.setData('roundcube-uri', href);
  361. 361 : dt.setData('roundcube-name', n.text().trim());
  362. 362 : }
  363. 363 : });
  364. 364 :
  365. 365 : this.check_mailvelope(this.env.action);
  366. 366 : }
  367. 367 : else if (this.env.action == 'compose') {
  368. 368 : this.env.address_group_stack = [];
  369. 369 : this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel',
  370. 370 : 'toggle-editor', 'list-addresses', 'pushgroup', 'search', 'reset-search', 'extwin',
  371. 371 : 'insert-response', 'save-response', 'menu-open', 'menu-close', 'load-attachment',
  372. 372 : 'download-attachment', 'open-attachment', 'rename-attachment'];
  373. 373 :
  374. 374 : if (this.env.drafts_mailbox)
  375. 375 : this.env.compose_commands.push('savedraft')
  376. 376 :
  377. 377 : this.enable_command(this.env.compose_commands, true);
  378. 378 :
  379. 379 : // add more commands (not enabled)
  380. 380 : $.merge(this.env.compose_commands, ['add-recipient', 'firstpage', 'previouspage', 'nextpage', 'lastpage']);
  381. 381 :
  382. 382 : if (window.googie) {
  383. 383 : this.env.editor_config.spellchecker = googie;
  384. 384 : this.env.editor_config.spellcheck_observer = function(s) { ref.spellcheck_state(); };
  385. 385 :
  386. 386 : this.env.compose_commands.push('spellcheck')
  387. 387 : this.enable_command('spellcheck', true);
  388. 388 : }
  389. 389 :
  390. 390 : // initialize HTML editor
  391. 391 : this.editor_init(this.env.editor_config, this.env.composebody);
  392. 392 :
  393. 393 : // init canned response functions
  394. 394 : if (this.gui_objects.responseslist) {
  395. 395 : $('a.insertresponse', this.gui_objects.responseslist)
  396. 396 : .attr('unselectable', 'on')
  397. 397 : .mousedown(function(e) { return rcube_event.cancel(e); })
  398. 398 : .on('mouseup keypress', function(e) {
  399. 399 : if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) {
  400. 400 : ref.command('insert-response', $(this).attr('rel'));
  401. 401 : $(document.body).trigger('mouseup'); // hides the menu
  402. 402 : return rcube_event.cancel(e);
  403. 403 : }
  404. 404 : });
  405. 405 :
  406. 406 : // avoid textarea loosing focus when hitting the save-response button/link
  407. 407 : $.each(this.buttons['save-response'] || [], function (i, v) {
  408. 408 : $('#' + v.id).mousedown(function(e){ return rcube_event.cancel(e); })
  409. 409 : });
  410. 410 : }
  411. 411 :
  412. 412 : // init message compose form
  413. 413 : this.init_messageform();
  414. 414 :
  415. 415 : this.check_mailvelope(this.env.action);
  416. 416 : }
  417. 417 : else if (this.env.action == 'bounce') {
  418. 418 : this.init_messageform_inputs();
  419. 419 : this.env.compose_commands = [];
  420. 420 : }
  421. 421 : else if (this.env.action == 'get') {
  422. 422 : this.enable_command('download', true);
  423. 423 : this.enable_command('image-scale', 'image-rotate', !!/^image\//.test(this.env.mimetype));
  424. 424 :
  425. 425 : // Mozilla's PDF.js viewer does not allow printing from host page (#5125)
  426. 426 : // to minimize user confusion we disable the Print button on Firefox < 75
  427. 427 : this.enable_command('print', this.env.mimetype != 'application/pdf' || !bw.mz || bw.vendver >= 75);
  428. 428 :
  429. 429 : if (this.env.is_message) {
  430. 430 : this.enable_command('reply', 'reply-all', 'edit', 'viewsource',
  431. 431 : 'forward', 'forward-inline', 'forward-attachment', 'bounce', true);
  432. 432 : if (this.env.list_post)
  433. 433 : this.enable_command('reply-list', true);
  434. 434 : }
  435. 435 :
  436. 436 : // center and scale the image in preview frame
  437. 437 : // TODO: Find a better way. Onload is late, also we could use embed.css
  438. 438 : if (this.env.mimetype.startsWith('image/'))
  439. 439 : $(this.gui_objects.messagepartframe).on('load', function() {
  440. 440 : var contents = $(this).contents();
  441. 441 :
  442. 442 : // do not apply styles to an error page (with no image)
  443. 443 : if (contents.find('img').length)
  444. 444 : contents.find('head').append(
  445. 445 : '<style type="text/css">'
  446. 446 : + 'img { max-width:100%; max-height:100%; } ' // scale
  447. 447 : + 'body { display:flex; align-items:center; justify-content:center; height:100%; margin:0; }' // align
  448. 448 : + '</style>'
  449. 449 : );
  450. 450 : });
  451. 451 : }
  452. 452 : // show printing dialog unless decryption must be done first
  453. 453 : else if (this.env.action == 'print' && this.env.uid) {
  454. 454 : this.check_mailvelope(this.env.action);
  455. 455 : if (!this.env.is_pgp_content && !this.env.pgp_mime_part) {
  456. 456 : this.print_dialog();
  457. 457 : }
  458. 458 : }
  459. 459 :
  460. 460 : // get unread count for each mailbox
  461. 461 : if (this.gui_objects.mailboxlist) {
  462. 462 : this.env.unread_counts = {};
  463. 463 : this.gui_objects.folderlist = this.gui_objects.mailboxlist;
  464. 464 : this.http_request('getunread', {_page: this.env.current_page});
  465. 465 : }
  466. 466 :
  467. 467 : // init address book widget
  468. 468 : if (this.gui_objects.contactslist) {
  469. 469 : this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
  470. 470 : { multiselect:true, draggable:false, keyboard:true });
  471. 471 : this.contact_list
  472. 472 : .addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); })
  473. 473 : .addEventListener('select', function(o) { ref.compose_recipient_select(o); })
  474. 474 : .addEventListener('dblclick', function(o) { ref.compose_add_recipient(); })
  475. 475 : .addEventListener('keypress', function(o) {
  476. 476 : if (o.key_pressed == o.ENTER_KEY) {
  477. 477 : if (!ref.compose_add_recipient()) {
  478. 478 : // execute link action on <enter> if not a recipient entry
  479. 479 : if (o.last_selected && String(o.last_selected).charAt(0) == 'G') {
  480. 480 : $(o.rows[o.last_selected].obj).find('a').first().click();
  481. 481 : }
  482. 482 : }
  483. 483 : }
  484. 484 : })
  485. 485 : .init();
  486. 486 :
  487. 487 : // remember last focused address field
  488. 488 : $('#_to,#_cc,#_bcc').focus(function() { ref.env.focused_field = this; });
  489. 489 : }
  490. 490 :
  491. 491 : if (this.gui_objects.addressbookslist) {
  492. 492 : this.gui_objects.folderlist = this.gui_objects.addressbookslist;
  493. 493 : this.enable_command('list-addresses', true);
  494. 494 : }
  495. 495 :
  496. 496 : // ask user to send MDN
  497. 497 : if (this.env.mdn_request && this.env.uid) {
  498. 498 : this.mdn_request_dialog(this.env.uid, this.env.mailbox);
  499. 499 : }
  500. 500 :
  501. 501 : // detect browser capabilities
  502. 502 : if (!this.is_framed() && !this.env.extwin)
  503. 503 : this.browser_capabilities_check();
  504. 504 :
  505. 505 : break;
  506. 506 :
  507. 507 : case 'addressbook':
  508. 508 : this.env.address_group_stack = [];
  509. 509 :
  510. 510 : if (this.gui_objects.folderlist)
  511. 511 : this.env.contactfolders = $.extend($.extend({}, this.env.address_sources), this.env.contactgroups);
  512. 512 :
  513. 513 : this.enable_command('add', 'import', this.env.writable_source);
  514. 514 : this.enable_command('list', 'listgroup', 'pushgroup', 'popgroup', 'listsearch', 'search', 'reset-search', 'advanced-search', true);
  515. 515 :
  516. 516 : if (this.gui_objects.contactslist) {
  517. 517 : this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
  518. 518 : {multiselect:true, draggable:this.gui_objects.folderlist?true:false, keyboard:true});
  519. 519 : this.contact_list
  520. 520 : .addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); })
  521. 521 : .addEventListener('keypress', function(o) { ref.list_keypress(o); })
  522. 522 : .addEventListener('select', function(o) { ref.contactlist_select(o); })
  523. 523 : .addEventListener('dragstart', function(o) { ref.drag_start(o); })
  524. 524 : .addEventListener('dragmove', function(e) { ref.drag_move(e); })
  525. 525 : .addEventListener('dragend', function(e) { ref.drag_end(e); })
  526. 526 : .init();
  527. 527 :
  528. 528 : $(this.gui_objects.qsearchbox).focusin(function() { ref.contact_list.blur(); });
  529. 529 :
  530. 530 : this.update_group_commands();
  531. 531 : this.command('list');
  532. 532 : }
  533. 533 :
  534. 534 : if (this.gui_objects.savedsearchlist) {
  535. 535 : this.savedsearchlist = new rcube_treelist_widget(this.gui_objects.savedsearchlist, {
  536. 536 : id_prefix: 'rcmli',
  537. 537 : id_encode: this.html_identifier_encode,
  538. 538 : id_decode: this.html_identifier_decode
  539. 539 : });
  540. 540 :
  541. 541 : this.savedsearchlist.addEventListener('select', function(node) {
  542. 542 : ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }); });
  543. 543 : }
  544. 544 :
  545. 545 : this.set_page_buttons();
  546. 546 :
  547. 547 : if (this.env.cid) {
  548. 548 : this.enable_command('show', 'edit', 'qrcode', true);
  549. 549 : // register handlers for group assignment via checkboxes
  550. 550 : if (this.gui_objects.editform) {
  551. 551 : $('input.groupmember').change(function() {
  552. 552 : ref.group_member_change(this.checked ? 'add' : 'del', ref.env.cid, ref.env.source, this.value);
  553. 553 : });
  554. 554 : }
  555. 555 : }
  556. 556 :
  557. 557 : if (this.gui_objects.editform) {
  558. 558 : this.enable_command('save', true);
  559. 559 : if (this.env.action == 'add' || this.env.action == 'edit' || this.env.action == 'search')
  560. 560 : this.init_contact_form();
  561. 561 : }
  562. 562 : else if (this.env.action == 'print') {
  563. 563 : this.print_dialog();
  564. 564 : }
  565. 565 :
  566. 566 : break;
  567. 567 :
  568. 568 : case 'settings':
  569. 569 : this.enable_command('show', 'save', true);
  570. 570 :
  571. 571 : if (this.env.action == 'identities') {
  572. 572 : this.enable_command('add', this.env.identities_level < 2);
  573. 573 : }
  574. 574 : else if (this.env.action == 'edit-identity' || this.env.action == 'add-identity') {
  575. 575 : this.enable_command('save', 'edit', 'toggle-editor', true);
  576. 576 : this.enable_command('delete', this.env.identities_level < 2);
  577. 577 :
  578. 578 : // initialize HTML editor
  579. 579 : this.editor_init(this.env.editor_config, 'rcmfd_signature');
  580. 580 :
  581. 581 : if (this.env.action == 'edit-identity') {
  582. 582 : this.check_mailvelope(this.env.action);
  583. 583 : }
  584. 584 : }
  585. 585 : else if (this.env.action == 'folders') {
  586. 586 : this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', true);
  587. 587 : }
  588. 588 : else if (this.env.action == 'edit-folder' && this.gui_objects.editform) {
  589. 589 : this.enable_command('save', 'folder-size', true);
  590. 590 : parent.rcmail.env.exists = this.env.messagecount;
  591. 591 : parent.rcmail.enable_command('purge', this.env.messagecount);
  592. 592 : }
  593. 593 : else if (this.env.action == 'responses') {
  594. 594 : this.enable_command('add', true);
  595. 595 : }
  596. 596 :
  597. 597 : if (this.gui_objects.identitieslist) {
  598. 598 : this.identity_list = new rcube_list_widget(this.gui_objects.identitieslist,
  599. 599 : {multiselect:false, draggable:false, keyboard:true});
  600. 600 : this.identity_list
  601. 601 : .addEventListener('select', function(o) { ref.identity_select(o); })
  602. 602 : .addEventListener('keypress', function(o) { ref.list_keypress(o); })
  603. 603 : .init()
  604. 604 : .focus();
  605. 605 : }
  606. 606 : else if (this.gui_objects.sectionslist) {
  607. 607 : this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:true});
  608. 608 : this.sections_list
  609. 609 : .addEventListener('select', function(o) { ref.section_select(o); })
  610. 610 : .init()
  611. 611 : .focus();
  612. 612 : }
  613. 613 : else if (this.gui_objects.subscriptionlist) {
  614. 614 : this.init_subscription_list();
  615. 615 : }
  616. 616 : else if (this.gui_objects.responseslist) {
  617. 617 : this.responses_list = new rcube_list_widget(this.gui_objects.responseslist, {multiselect:false, draggable:false, keyboard:true});
  618. 618 : this.responses_list
  619. 619 : .addEventListener('select', function(o) { ref.response_select(o); })
  620. 620 : .addEventListener('keypress', function(o) { ref.list_keypress(o); })
  621. 621 : .init()
  622. 622 : .focus();
  623. 623 : }
  624. 624 :
  625. 625 : break;
  626. 626 :
  627. 627 : case 'login':
  628. 628 : var tz, tz_name,
  629. 629 : input_user = $('#rcmloginuser'),
  630. 630 : input_tz = $('#rcmlogintz');
  631. 631 :
  632. 632 : if (input_user.val() == '')
  633. 633 : input_user.focus();
  634. 634 : else
  635. 635 : $('#rcmloginpwd').focus();
  636. 636 :
  637. 637 : // detect client timezone
  638. 638 : if (window.jstz && (tz = jstz.determine()))
  639. 639 : tz_name = tz.name();
  640. 640 :
  641. 641 : input_tz.val(tz_name ? tz_name : (new Date().getStdTimezoneOffset() / -60));
  642. 642 :
  643. 643 : // display 'loading' message on form submit, lock submit button
  644. 644 : $('form').submit(function () {
  645. 645 : $('[type=submit]', this).prop('disabled', true);
  646. 646 : ref.clear_messages();
  647. 647 : ref.display_message('', 'loading');
  648. 648 : });
  649. 649 :
  650. 650 : break;
  651. 651 : }
  652. 652 :
  653. 653 : // select first input field in an edit form
  654. 654 : if (this.gui_objects.editform)
  655. 655 : $("input,select,textarea", this.gui_objects.editform)
  656. 656 : .not(':hidden').not(':disabled').first().select().focus();
  657. 657 :
  658. 658 : // prevent from form submit with Enter key in file input fields
  659. 659 : if (bw.ie)
  660. 660 : $('input[type=file]').keydown(function(e) { if (e.keyCode == '13') e.preventDefault(); });
  661. 661 :
  662. 662 : // flag object as complete
  663. 663 : this.loaded = true;
  664. 664 : this.env.lastrefresh = new Date();
  665. 665 :
  666. 666 : // show message
  667. 667 : if (this.pending_message)
  668. 668 : this.display_message.apply(this, this.pending_message);
  669. 669 :
  670. 670 : // init treelist widget
  671. 671 : if (this.gui_objects.folderlist && window.rcube_treelist_widget
  672. 672 : // some plugins may load rcube_treelist_widget and there's one case
  673. 673 : // when this will cause problems - addressbook widget in compose,
  674. 674 : // which already has been initialized using rcube_list_widget
  675. 675 : && this.gui_objects.folderlist != this.gui_objects.addressbookslist
  676. 676 : ) {
  677. 677 : this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, {
  678. 678 : selectable: true,
  679. 679 : id_prefix: 'rcmli',
  680. 680 : parent_focus: true,
  681. 681 : id_encode: this.html_identifier_encode,
  682. 682 : id_decode: this.html_identifier_decode,
  683. 683 : check_droptarget: function(node) { return !node.virtual && ref.check_droptarget(node.id) }
  684. 684 : });
  685. 685 :
  686. 686 : this.treelist
  687. 687 : .addEventListener('collapse', function(node) { ref.folder_collapsed(node) })
  688. 688 : .addEventListener('expand', function(node) { ref.folder_collapsed(node) })
  689. 689 : .addEventListener('beforeselect', function(node) { return !ref.busy; })
  690. 690 : .addEventListener('select', function(node) {
  691. 691 : ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' });
  692. 692 : ref.mark_all_read_state();
  693. 693 : });
  694. 694 : }
  695. 695 :
  696. 696 : // activate html5 file drop feature (if browser supports it and if configured)
  697. 697 : if (this.gui_objects.filedrop && this.env.filedrop && window.FormData) {
  698. 698 : $(document.body).on('dragover dragleave drop', function(e) { return ref.document_drag_hover(e, e.type == 'dragover'); });
  699. 699 : $(this.gui_objects.filedrop).addClass('droptarget')
  700. 700 : .on('dragover dragleave', function(e) { return ref.file_drag_hover(e, e.type == 'dragover'); })
  701. 701 : .get(0).addEventListener('drop', function(e) { return ref.file_dropped(e); }, false);
  702. 702 : }
  703. 703 :
  704. 704 : // catch document (and iframe) mouse clicks
  705. 705 : var body_mouseup = function(e) { return ref.doc_mouse_up(e); };
  706. 706 : $(document.body)
  707. 707 : .mouseup(body_mouseup)
  708. 708 : .keydown(function(e) { return ref.doc_keypress(e); });
  709. 709 :
  710. 710 : rcube_webmail.set_iframe_events({mouseup: body_mouseup});
  711. 711 :
  712. 712 : // trigger init event hook
  713. 713 : this.triggerEvent('init', { task:this.task, action:this.env.action });
  714. 714 :
  715. 715 : // execute all foreign onload scripts
  716. 716 : // @deprecated
  717. 717 : for (n in this.onloads) {
  718. 718 : if (typeof this.onloads[n] === 'string')
  719. 719 : eval(this.onloads[n]);
  720. 720 : else if (typeof this.onloads[n] === 'function')
  721. 721 : this.onloads[n]();
  722. 722 : }
  723. 723 :
  724. 724 : // register menu buttons
  725. 725 : $('[data-popup]').each(function() { ref.register_menu_button(this, $(this).data('popup')); });
  726. 726 :
  727. 727 : // start keep-alive and refresh intervals
  728. 728 : this.start_refresh();
  729. 729 : this.start_keepalive();
  730. 730 : };
  731. 731 :
  732. 732 : this.log = function(msg)
  733. 733 : {
  734. 734 : if (this.env.devel_mode && window.console && console.log)
  735. 735 : console.log(msg);
  736. 736 : };
  737. 737 :
  738. 738 : /*********************************************************/
  739. 739 : /********* client command interface *********/
  740. 740 : /*********************************************************/
  741. 741 :
  742. 742 : // execute a specific command on the web client
  743. 743 : this.command = function(command, props, obj, event, allow_disabled)
  744. 744 : {
  745. 745 : var ret;
  746. 746 :
  747. 747 : if (obj && obj.blur && !(event && rcube_event.is_keyboard(event)))
  748. 748 : obj.blur();
  749. 749 :
  750. 750 : // do nothing if interface is locked by another command
  751. 751 : // with exception for searching reset and menu
  752. 752 : if (this.busy && !(command == 'reset-search' && this.last_command == 'search') && !command.match(/^menu-/))
  753. 753 : return false;
  754. 754 :
  755. 755 : // let the browser handle this click (shift/ctrl usually opens the link in a new window/tab)
  756. 756 : if ((obj && obj.href && String(obj.href).indexOf('#') < 0) && rcube_event.get_modifier(event)) {
  757. 757 : return true;
  758. 758 : }
  759. 759 :
  760. 760 : // command not supported or allowed
  761. 761 : if (!allow_disabled && !this.commands[command]) {
  762. 762 : // pass command to parent window
  763. 763 : if (this.is_framed())
  764. 764 : parent.rcmail.command(command, props);
  765. 765 :
  766. 766 : return false;
  767. 767 : }
  768. 768 :
  769. 769 : // check input before leaving compose step
  770. 770 : if (this.task == 'mail' && this.env.action == 'compose' && !this.env.server_error && command != 'save-pref'
  771. 771 : && ($.inArray(command, this.env.compose_commands) < 0 || command.startsWith('compose-encrypted') && ref.mailvelope_editor)
  772. 772 : && !this.compose_skip_unsavedcheck
  773. 773 : ) {
  774. 774 : if (!this.env.is_sent && this.cmp_hash != this.compose_field_hash()) {
  775. 775 : this.confirm_dialog(this.get_label('notsentwarning'), 'discard', function() {
  776. 776 : // remove copy from local storage if compose screen is left intentionally
  777. 777 : ref.remove_compose_data(ref.env.compose_id);
  778. 778 : ref.compose_skip_unsavedcheck = true;
  779. 779 : ref.command(command, props, obj, event);
  780. 780 : });
  781. 781 :
  782. 782 : return false;
  783. 783 : }
  784. 784 : }
  785. 785 :
  786. 786 : this.last_command = command;
  787. 787 : this.command_aborted = false;
  788. 788 :
  789. 789 : // trigger plugin hooks
  790. 790 : this.triggerEvent('actionbefore', {props: props, action: command, originalEvent: event});
  791. 791 :
  792. 792 : if ((ret = this.triggerEvent('before' + command, props || event)) !== undefined) {
  793. 793 : // abort if one of the handlers returned false
  794. 794 : if (ret === false)
  795. 795 : return false;
  796. 796 :
  797. 797 : props = ret;
  798. 798 : }
  799. 799 :
  800. 800 : // process external commands
  801. 801 : if (typeof this.command_handlers[command] === 'function') {
  802. 802 : ret = this.command_handlers[command](props, obj, event);
  803. 803 : }
  804. 804 : else if (typeof this.command_handlers[command] === 'string') {
  805. 805 : ret = window[this.command_handlers[command]](props, obj, event);
  806. 806 : }
  807. 807 : // process internal commands
  808. 808 : else {
  809. 809 : ret = this.command_handler(command, props, obj, event);
  810. 810 : }
  811. 811 :
  812. 812 : if (!this.command_aborted && this.triggerEvent('after' + command, props) === false)
  813. 813 : ret = false;
  814. 814 :
  815. 815 : this.triggerEvent('actionafter', {props: props, action: command, aborted: this.command_aborted, ret: ret, originalEvent: event});
  816. 816 :
  817. 817 : if (ret === false)
  818. 818 : return false;
  819. 819 :
  820. 820 : if ((obj && ret !== true) || this.command_aborted === true)
  821. 821 : return false;
  822. 822 :
  823. 823 : return true;
  824. 824 : };
  825. 825 :
  826. 826 : // execute a specific known command
  827. 827 : this.command_handler = function(command, props, obj, event)
  828. 828 : {
  829. 829 : var uid, cid, url, flag;
  830. 830 :
  831. 831 : // process internal command
  832. 832 : switch (command) {
  833. 833 :
  834. 834 : // commands to switch task
  835. 835 : case 'logout':
  836. 836 : case 'mail':
  837. 837 : case 'addressbook':
  838. 838 : case 'settings':
  839. 839 : this.switch_task(command);
  840. 840 : break;
  841. 841 :
  842. 842 : case 'about':
  843. 843 : this.redirect('?_task=settings&_action=about', false);
  844. 844 : break;
  845. 845 :
  846. 846 : case 'permaurl':
  847. 847 : if (obj && obj.href && obj.target)
  848. 848 : return true;
  849. 849 : if (this.env.permaurl)
  850. 850 : parent.location.href = this.env.permaurl;
  851. 851 : break;
  852. 852 :
  853. 853 : case 'extwin':
  854. 854 : if (this.env.action == 'compose') {
  855. 855 : var form = this.gui_objects.messageform,
  856. 856 : win = this.open_window('');
  857. 857 :
  858. 858 : if (win) {
  859. 859 : this.save_compose_form_local();
  860. 860 : this.compose_skip_unsavedcheck = true;
  861. 861 : $("[name='_action']", form).val('compose');
  862. 862 : form.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 });
  863. 863 : form.target = win.name;
  864. 864 : form.submit();
  865. 865 : }
  866. 866 : }
  867. 867 : else {
  868. 868 : this.open_window(this.env.permaurl, true);
  869. 869 : }
  870. 870 : break;
  871. 871 :
  872. 872 : case 'change-format':
  873. 873 : url = this.env.permaurl + '&_format=' + props;
  874. 874 :
  875. 875 : if (this.env.action == 'preview')
  876. 876 : url = url.replace(/_action=show/, '_action=preview') + '&_framed=1';
  877. 877 : if (this.env.extwin)
  878. 878 : url += '&_extwin=1';
  879. 879 :
  880. 880 : location.href = url;
  881. 881 : break;
  882. 882 :
  883. 883 : case 'menu-open':
  884. 884 : if (props && props.menu == 'attachmentmenu') {
  885. 885 : var mimetype = this.env.attachments[props.id];
  886. 886 : if (mimetype && mimetype.mimetype) // in compose format is different
  887. 887 : mimetype = mimetype.mimetype;
  888. 888 : this.enable_command('open-attachment', mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0);
  889. 889 : }
  890. 890 : this.show_menu(props, props.show || undefined, event);
  891. 891 : break;
  892. 892 :
  893. 893 : case 'menu-close':
  894. 894 : this.hide_menu(props, event);
  895. 895 : break;
  896. 896 :
  897. 897 : case 'menu-save':
  898. 898 : this.triggerEvent(command, {props:props, originalEvent:event});
  899. 899 : return false;
  900. 900 :
  901. 901 : case 'open':
  902. 902 : if (uid = this.get_single_uid()) {
  903. 903 : obj.href = this.url('show', this.params_from_uid(uid, {_extwin: 1}));
  904. 904 : return true;
  905. 905 : }
  906. 906 : break;
  907. 907 :
  908. 908 : case 'close':
  909. 909 : if (this.env.extwin)
  910. 910 : window.close();
  911. 911 : break;
  912. 912 :
  913. 913 : case 'list':
  914. 914 : if (props && props != '') {
  915. 915 : this.reset_qsearch(true);
  916. 916 : }
  917. 917 : if (this.env.action == 'compose' && this.env.extwin) {
  918. 918 : window.close();
  919. 919 : }
  920. 920 : else if (this.task == 'mail') {
  921. 921 : this.list_mailbox(props, props ? 1 : '');
  922. 922 : this.set_button_titles();
  923. 923 : }
  924. 924 : else if (this.task == 'addressbook')
  925. 925 : this.list_contacts(props);
  926. 926 : break;
  927. 927 :
  928. 928 : case 'set-listmode':
  929. 929 : this.set_list_options(null, undefined, undefined, props == 'threads' ? 1 : 0);
  930. 930 : break;
  931. 931 :
  932. 932 : case 'sort':
  933. 933 : var sort_order = this.env.sort_order,
  934. 934 : sort_col = !this.env.disabled_sort_col ? props : this.env.sort_col;
  935. 935 :
  936. 936 : if (!this.env.disabled_sort_order)
  937. 937 : sort_order = this.env.sort_col == sort_col && sort_order == 'ASC' ? 'DESC' : 'ASC';
  938. 938 :
  939. 939 : // set table header and update env
  940. 940 : this.set_list_sorting(sort_col, sort_order);
  941. 941 :
  942. 942 : // reload message list
  943. 943 : this.list_mailbox('', '', sort_col+'_'+sort_order);
  944. 944 : break;
  945. 945 :
  946. 946 : case 'nextpage':
  947. 947 : this.list_page('next');
  948. 948 : break;
  949. 949 :
  950. 950 : case 'lastpage':
  951. 951 : this.list_page('last');
  952. 952 : break;
  953. 953 :
  954. 954 : case 'previouspage':
  955. 955 : this.list_page('prev');
  956. 956 : break;
  957. 957 :
  958. 958 : case 'firstpage':
  959. 959 : this.list_page('first');
  960. 960 : break;
  961. 961 :
  962. 962 : case 'expunge':
  963. 963 : if (this.env.exists)
  964. 964 : this.expunge_mailbox(this.env.mailbox);
  965. 965 : break;
  966. 966 :
  967. 967 : case 'purge':
  968. 968 : case 'empty-mailbox':
  969. 969 : if (this.env.exists)
  970. 970 : this.purge_mailbox(this.env.mailbox);
  971. 971 : break;
  972. 972 :
  973. 973 : // common commands used in multiple tasks
  974. 974 : case 'show':
  975. 975 : if (this.task == 'mail') {
  976. 976 : uid = this.get_single_uid();
  977. 977 : if (uid && (!this.env.uid || uid != this.env.uid)) {
  978. 978 : var mbox = this.get_message_mailbox(uid);
  979. 979 : if (mbox == this.env.drafts_mailbox)
  980. 980 : this.open_compose_step({_draft_uid: uid, _mbox: mbox});
  981. 981 : else
  982. 982 : this.show_message(uid);
  983. 983 : }
  984. 984 : }
  985. 985 : else if (this.task == 'addressbook') {
  986. 986 : cid = props ? props : this.get_single_cid();
  987. 987 : if (cid && !(this.env.action == 'show' && cid == this.env.cid))
  988. 988 : this.load_contact(cid, 'show');
  989. 989 : }
  990. 990 : else if (this.task == 'settings') {
  991. 991 : this.goto_url('settings/' + props, {_framed: 0});
  992. 992 : }
  993. 993 : break;
  994. 994 :
  995. 995 : case 'add':
  996. 996 : if (this.task == 'addressbook')
  997. 997 : this.load_contact(0, 'add');
  998. 998 : else if (this.task == 'settings' && this.env.action == 'responses')
  999. 999 : this.load_response(0, 'add-response');
  1000. 1000 : else if (this.task == 'settings')
  1001. 1001 : this.load_identity(0, 'add-identity');
  1002. 1002 : break;
  1003. 1003 :
  1004. 1004 : case 'edit':
  1005. 1005 : if (this.task == 'addressbook' && (cid = this.get_single_cid()))
  1006. 1006 : this.load_contact(cid, 'edit');
  1007. 1007 : else if (this.task == 'mail' && (uid = this.get_single_uid())) {
  1008. 1008 : url = { _mbox: this.get_message_mailbox(uid) };
  1009. 1009 : url[url._mbox == this.env.drafts_mailbox && props != 'new' ? '_draft_uid' : '_uid'] = uid;
  1010. 1010 : this.open_compose_step(url);
  1011. 1011 : }
  1012. 1012 : break;
  1013. 1013 :
  1014. 1014 : case 'save':
  1015. 1015 : var input, form = this.gui_objects.editform;
  1016. 1016 : if (form) {
  1017. 1017 : // user prefs
  1018. 1018 : if ((input = $("[name='_pagesize']", form)) && input.length && isNaN(parseInt(input.val()))) {
  1019. 1019 : this.alert_dialog(this.get_label('nopagesizewarning'), function() {
  1020. 1020 : input.focus();
  1021. 1021 : });
  1022. 1022 : break;
  1023. 1023 : }
  1024. 1024 : // contacts/identities
  1025. 1025 : else {
  1026. 1026 : // reload form
  1027. 1027 : if (props == 'reload') {
  1028. 1028 : form.action += '&_reload=1';
  1029. 1029 : }
  1030. 1030 : else if (this.task == 'settings' && (this.env.identities_level % 2) == 0 &&
  1031. 1031 : (input = $("[name='_email']", form)) && input.length && !rcube_check_email(input.val())
  1032. 1032 : ) {
  1033. 1033 : this.alert_dialog(this.get_label('noemailwarning'), function() {
  1034. 1034 : input.focus();
  1035. 1035 : });
  1036. 1036 : break;
  1037. 1037 : }
  1038. 1038 : }
  1039. 1039 :
  1040. 1040 : // add selected source (on the list)
  1041. 1041 : if (parent.rcmail && parent.rcmail.env.source)
  1042. 1042 : form.action = this.add_url(form.action, '_orig_source', parent.rcmail.env.source);
  1043. 1043 :
  1044. 1044 : form.submit();
  1045. 1045 : }
  1046. 1046 : break;
  1047. 1047 :
  1048. 1048 : case 'delete':
  1049. 1049 : // mail task
  1050. 1050 : if (this.task == 'mail')
  1051. 1051 : this.delete_messages(event);
  1052. 1052 : // addressbook task
  1053. 1053 : else if (this.task == 'addressbook')
  1054. 1054 : this.delete_contacts();
  1055. 1055 : // settings: canned response
  1056. 1056 : else if (this.task == 'settings' && this.env.action == 'responses')
  1057. 1057 : this.delete_response();
  1058. 1058 : // settings: user identities
  1059. 1059 : else if (this.task == 'settings')
  1060. 1060 : this.delete_identity();
  1061. 1061 : break;
  1062. 1062 :
  1063. 1063 : // mail task commands
  1064. 1064 : case 'move':
  1065. 1065 : case 'moveto': // deprecated
  1066. 1066 : if (this.task == 'mail')
  1067. 1067 : this.move_messages(props, event);
  1068. 1068 : else if (this.task == 'addressbook')
  1069. 1069 : this.move_contacts(props, event);
  1070. 1070 : break;
  1071. 1071 :
  1072. 1072 : case 'copy':
  1073. 1073 : if (this.task == 'mail')
  1074. 1074 : this.copy_messages(props, event);
  1075. 1075 : else if (this.task == 'addressbook')
  1076. 1076 : this.copy_contacts(props, event);
  1077. 1077 : break;
  1078. 1078 :
  1079. 1079 : case 'mark':
  1080. 1080 : if (props)
  1081. 1081 : this.mark_message(props);
  1082. 1082 : break;
  1083. 1083 :
  1084. 1084 : case 'toggle_status':
  1085. 1085 : case 'toggle_flag':
  1086. 1086 : flag = command == 'toggle_flag' ? 'flagged' : 'read';
  1087. 1087 :
  1088. 1088 : if (uid = props) {
  1089. 1089 : // toggle flagged/unflagged
  1090. 1090 : if (flag == 'flagged') {
  1091. 1091 : if (this.message_list.rows[uid].flagged)
  1092. 1092 : flag = 'unflagged';
  1093. 1093 : }
  1094. 1094 : // toggle read/unread
  1095. 1095 : else if (this.message_list.rows[uid].deleted)
  1096. 1096 : flag = 'undelete';
  1097. 1097 : else if (!this.message_list.rows[uid].unread)
  1098. 1098 : flag = 'unread';
  1099. 1099 :
  1100. 1100 : this.mark_message(flag, uid);
  1101. 1101 : }
  1102. 1102 :
  1103. 1103 : break;
  1104. 1104 :
  1105. 1105 : case 'add-contact':
  1106. 1106 : this.add_contact(props);
  1107. 1107 : break;
  1108. 1108 :
  1109. 1109 : case 'load-remote':
  1110. 1110 : if (this.env.uid) {
  1111. 1111 : if (props && this.env.sender) {
  1112. 1112 : this.add_contact(this.env.sender, true, props);
  1113. 1113 : break;
  1114. 1114 : }
  1115. 1115 :
  1116. 1116 : this.show_message(this.env.uid, true, this.env.action == 'preview');
  1117. 1117 : }
  1118. 1118 : break;
  1119. 1119 :
  1120. 1120 : case 'load-attachment':
  1121. 1121 : case 'open-attachment':
  1122. 1122 : case 'download-attachment':
  1123. 1123 : var params, mimetype = this.env.attachments[props];
  1124. 1124 :
  1125. 1125 : if (this.env.action == 'compose') {
  1126. 1126 : params = {_file: props, _id: this.env.compose_id};
  1127. 1127 : mimetype = mimetype ? mimetype.mimetype : '';
  1128. 1128 : }
  1129. 1129 : else {
  1130. 1130 : params = {_mbox: this.env.mailbox, _uid: this.env.uid, _part: props};
  1131. 1131 : }
  1132. 1132 :
  1133. 1133 : // open attachment in frame if it's of a supported mimetype
  1134. 1134 : if (command != 'download-attachment' && mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0) {
  1135. 1135 : // Note: We disable _framed for proper X-Frame-Options:deny support (#6688)
  1136. 1136 : if (this.open_window(this.url('get', $.extend({_frame: 1, _framed: 0}, params))))
  1137. 1137 : break;
  1138. 1138 : }
  1139. 1139 :
  1140. 1140 : params._download = 1;
  1141. 1141 :
  1142. 1142 : // prevent from page unload warning in compose
  1143. 1143 : this.compose_skip_unsavedcheck = 1;
  1144. 1144 : this.goto_url('get', params, false, true);
  1145. 1145 : this.compose_skip_unsavedcheck = 0;
  1146. 1146 :
  1147. 1147 : break;
  1148. 1148 :
  1149. 1149 : case 'select-all':
  1150. 1150 : this.select_all_mode = props ? false : true;
  1151. 1151 : this.dummy_select = true; // prevent msg opening if there's only one msg on the list
  1152. 1152 : var list = this[this.task == 'addressbook' ? 'contact_list' : 'message_list'];
  1153. 1153 : if (props == 'invert')
  1154. 1154 : list.invert_selection();
  1155. 1155 : else
  1156. 1156 : list.select_all(props == 'page' ? '' : props);
  1157. 1157 : this.dummy_select = null;
  1158. 1158 : break;
  1159. 1159 :
  1160. 1160 : case 'select-none':
  1161. 1161 : this.select_all_mode = false;
  1162. 1162 : this[this.task == 'addressbook' ? 'contact_list' : 'message_list'].clear_selection();
  1163. 1163 : break;
  1164. 1164 :
  1165. 1165 : case 'expand-all':
  1166. 1166 : this.env.autoexpand_threads = 1;
  1167. 1167 : this.message_list.expand_all();
  1168. 1168 : break;
  1169. 1169 :
  1170. 1170 : case 'expand-unread':
  1171. 1171 : this.env.autoexpand_threads = 2;
  1172. 1172 : this.message_list.collapse_all();
  1173. 1173 : this.expand_unread();
  1174. 1174 : break;
  1175. 1175 :
  1176. 1176 : case 'collapse-all':
  1177. 1177 : this.env.autoexpand_threads = 0;
  1178. 1178 : this.message_list.collapse_all();
  1179. 1179 : break;
  1180. 1180 :
  1181. 1181 : case 'nextmessage':
  1182. 1182 : if (this.env.next_uid)
  1183. 1183 : this.show_message(this.env.next_uid, false, this.env.action == 'preview');
  1184. 1184 : break;
  1185. 1185 :
  1186. 1186 : case 'lastmessage':
  1187. 1187 : if (this.env.last_uid)
  1188. 1188 : this.show_message(this.env.last_uid);
  1189. 1189 : break;
  1190. 1190 :
  1191. 1191 : case 'previousmessage':
  1192. 1192 : if (this.env.prev_uid)
  1193. 1193 : this.show_message(this.env.prev_uid, false, this.env.action == 'preview');
  1194. 1194 : break;
  1195. 1195 :
  1196. 1196 : case 'firstmessage':
  1197. 1197 : if (this.env.first_uid)
  1198. 1198 : this.show_message(this.env.first_uid);
  1199. 1199 : break;
  1200. 1200 :
  1201. 1201 : case 'compose':
  1202. 1202 : url = {};
  1203. 1203 :
  1204. 1204 : if (this.task == 'mail') {
  1205. 1205 : url = {_mbox: this.env.mailbox, _search: this.env.search_request};
  1206. 1206 : if (props)
  1207. 1207 : url._to = props;
  1208. 1208 : }
  1209. 1209 : // modify url if we're in addressbook
  1210. 1210 : else if (this.task == 'addressbook') {
  1211. 1211 : // switch to mail compose step directly
  1212. 1212 : if (props && props.indexOf('@') > 0) {
  1213. 1213 : url._to = props;
  1214. 1214 : }
  1215. 1215 : else {
  1216. 1216 : var a_cids = [];
  1217. 1217 : // use contact id passed as command parameter
  1218. 1218 : if (props)
  1219. 1219 : a_cids.push(props);
  1220. 1220 : // get selected contacts
  1221. 1221 : else if (this.contact_list)
  1222. 1222 : a_cids = this.contact_list.get_selection();
  1223. 1223 :
  1224. 1224 : if (a_cids.length) {
  1225. 1225 : this.http_post('mailto', { _cid: a_cids.join(','), _source: this.env.source }, true);
  1226. 1226 : break;
  1227. 1227 : }
  1228. 1228 : else if (this.env.group && this.env.pagecount) {
  1229. 1229 : this.http_post('mailto', { _gid: this.env.group, _source: this.env.source }, true);
  1230. 1230 : break;
  1231. 1231 : }
  1232. 1232 : }
  1233. 1233 : }
  1234. 1234 : else if (props && typeof props == 'string') {
  1235. 1235 : url._to = props;
  1236. 1236 : }
  1237. 1237 : else if (props && typeof props == 'object') {
  1238. 1238 : $.extend(url, props);
  1239. 1239 : }
  1240. 1240 :
  1241. 1241 : this.open_compose_step(url);
  1242. 1242 : break;
  1243. 1243 :
  1244. 1244 : case 'spellcheck':
  1245. 1245 : if (this.spellcheck_state()) {
  1246. 1246 : this.editor.spellcheck_stop();
  1247. 1247 : }
  1248. 1248 : else {
  1249. 1249 : this.editor.spellcheck_start();
  1250. 1250 : }
  1251. 1251 : break;
  1252. 1252 :
  1253. 1253 : case 'savedraft':
  1254. 1254 : // Reset the auto-save timer
  1255. 1255 : clearTimeout(this.save_timer);
  1256. 1256 :
  1257. 1257 : // compose form did not change (and draft wasn't saved already)
  1258. 1258 : if (this.env.draft_id && this.cmp_hash == this.compose_field_hash()) {
  1259. 1259 : this.auto_save_start();
  1260. 1260 : break;
  1261. 1261 : }
  1262. 1262 :
  1263. 1263 : this.submit_messageform(true);
  1264. 1264 : break;
  1265. 1265 :
  1266. 1266 : case 'send':
  1267. 1267 : if (!props.nocheck && !this.env.is_sent && !this.check_compose_input(command))
  1268. 1268 : break;
  1269. 1269 :
  1270. 1270 : // Reset the auto-save timer
  1271. 1271 : clearTimeout(this.save_timer);
  1272. 1272 :
  1273. 1273 : this.submit_messageform();
  1274. 1274 : break;
  1275. 1275 :
  1276. 1276 : case 'send-attachment':
  1277. 1277 : // Reset the auto-save timer
  1278. 1278 : clearTimeout(this.save_timer);
  1279. 1279 :
  1280. 1280 : if (!(flag = this.upload_file(props || this.gui_objects.uploadform, 'upload'))) {
  1281. 1281 : if (flag !== false)
  1282. 1282 : this.alert_dialog(this.get_label('selectimportfile'));
  1283. 1283 : aborted = true;
  1284. 1284 : }
  1285. 1285 : break;
  1286. 1286 :
  1287. 1287 : case 'insert-sig':
  1288. 1288 : this.change_identity($("[name='_from']")[0], true);
  1289. 1289 : break;
  1290. 1290 :
  1291. 1291 : case 'list-addresses':
  1292. 1292 : this.list_contacts(props);
  1293. 1293 : this.enable_command('add-recipient', false);
  1294. 1294 : break;
  1295. 1295 :
  1296. 1296 : case 'add-recipient':
  1297. 1297 : this.compose_add_recipient(props);
  1298. 1298 : break;
  1299. 1299 :
  1300. 1300 : case 'reply-all':
  1301. 1301 : case 'reply-list':
  1302. 1302 : case 'reply':
  1303. 1303 : if (uid = this.get_single_uid()) {
  1304. 1304 : url = {_reply_uid: uid, _mbox: this.get_message_mailbox(uid), _search: this.env.search_request};
  1305. 1305 : if (command == 'reply-all')
  1306. 1306 : // do reply-list, when list is detected and popup menu wasn't used
  1307. 1307 : url._all = (!props && this.env.reply_all_mode == 1 && this.commands['reply-list'] ? 'list' : 'all');
  1308. 1308 : else if (command == 'reply-list')
  1309. 1309 : url._all = 'list';
  1310. 1310 :
  1311. 1311 : this.open_compose_step(url);
  1312. 1312 : }
  1313. 1313 : break;
  1314. 1314 :
  1315. 1315 : case 'forward-attachment':
  1316. 1316 : case 'forward-inline':
  1317. 1317 : case 'forward':
  1318. 1318 : var uids = this.env.uid ? [this.env.uid] : (this.message_list ? this.message_list.get_selection() : []);
  1319. 1319 : if (uids.length) {
  1320. 1320 : url = { _forward_uid: this.uids_to_list(uids), _mbox: this.env.mailbox, _search: this.env.search_request };
  1321. 1321 : if (command == 'forward-attachment' || (!props && this.env.forward_attachment) || uids.length > 1)
  1322. 1322 : url._attachment = 1;
  1323. 1323 : this.open_compose_step(url);
  1324. 1324 : }
  1325. 1325 : break;
  1326. 1326 :
  1327. 1327 : case 'print':
  1328. 1328 : if (this.task == 'addressbook') {
  1329. 1329 : if (uid = this.get_single_cid()) {
  1330. 1330 : url = '&_action=print&_cid=' + uid;
  1331. 1331 : if (this.env.source)
  1332. 1332 : url += '&_source=' + urlencode(this.env.source);
  1333. 1333 : this.open_window(this.env.comm_path + url, true, true);
  1334. 1334 : }
  1335. 1335 : }
  1336. 1336 : else if (this.env.action == 'get' && !this.env.is_message) {
  1337. 1337 : this.gui_objects.messagepartframe.contentWindow.print();
  1338. 1338 : }
  1339. 1339 : else if (uid = this.get_single_uid()) {
  1340. 1340 : url = this.url('print', this.params_from_uid(uid, {_safe: this.env.safemode ? 1 : 0}));
  1341. 1341 : if (this.open_window(url, true, true)) {
  1342. 1342 : if (this.env.action != 'show' && this.env.action != 'get')
  1343. 1343 : this.mark_message('read', uid);
  1344. 1344 : }
  1345. 1345 : }
  1346. 1346 : break;
  1347. 1347 :
  1348. 1348 : case 'viewsource':
  1349. 1349 : if (uid = this.get_single_uid())
  1350. 1350 : this.open_window(this.url('viewsource', this.params_from_uid(uid)), true, true);
  1351. 1351 : break;
  1352. 1352 :
  1353. 1353 : case 'download':
  1354. 1354 : if (this.env.action == 'get') {
  1355. 1355 : location.href = this.secure_url(location.href.replace(/_frame=/, '_download='));
  1356. 1356 : }
  1357. 1357 : else if (uid = this.get_single_uid()) {
  1358. 1358 : this.goto_url('viewsource', this.params_from_uid(uid, {_save: 1}), false, true);
  1359. 1359 : }
  1360. 1360 : break;
  1361. 1361 :
  1362. 1362 : // quicksearch
  1363. 1363 : case 'search':
  1364. 1364 : return this.qsearch(props);
  1365. 1365 :
  1366. 1366 : // reset quicksearch
  1367. 1367 : case 'reset-search':
  1368. 1368 : var n, s = this.env.search_request || this.env.qsearch;
  1369. 1369 :
  1370. 1370 : this.reset_qsearch(true);
  1371. 1371 :
  1372. 1372 : if (s && this.env.action == 'compose') {
  1373. 1373 : if (this.contact_list)
  1374. 1374 : this.list_contacts_clear();
  1375. 1375 : }
  1376. 1376 : else if (s && this.env.mailbox) {
  1377. 1377 : this.list_mailbox(this.env.mailbox, 1);
  1378. 1378 : }
  1379. 1379 : else if (s && this.task == 'addressbook') {
  1380. 1380 : this.env.source = this.env.last_source || '';
  1381. 1381 : this.env.group = this.env.last_group || '';
  1382. 1382 : this.list_contacts(this.env.source, this.env.group, 1);
  1383. 1383 : }
  1384. 1384 : break;
  1385. 1385 :
  1386. 1386 : case 'pushgroup':
  1387. 1387 : // add group ID and current search to stack
  1388. 1388 : var group = {
  1389. 1389 : id: props.id,
  1390. 1390 : search_request: this.env.search_request,
  1391. 1391 : page: this.env.current_page,
  1392. 1392 : search: this.env.search_request && this.gui_objects.qsearchbox ? this.gui_objects.qsearchbox.value : null
  1393. 1393 : };
  1394. 1394 :
  1395. 1395 : this.env.address_group_stack.push(group);
  1396. 1396 : if (obj && event)
  1397. 1397 : rcube_event.cancel(event);
  1398. 1398 :
  1399. 1399 : // FIXME: no break?
  1400. 1400 :
  1401. 1401 : case 'listgroup':
  1402. 1402 : this.reset_qsearch();
  1403. 1403 : this.list_contacts(props.source, props.id, 1, group);
  1404. 1404 : break;
  1405. 1405 :
  1406. 1406 : case 'popgroup':
  1407. 1407 : if (this.env.address_group_stack.length) {
  1408. 1408 : var old = this.env.address_group_stack.pop();
  1409. 1409 : this.reset_qsearch();
  1410. 1410 :
  1411. 1411 : if (old.search_request) {
  1412. 1412 : // this code is executed when going back to the search result
  1413. 1413 : if (old.search && this.gui_objects.qsearchbox)
  1414. 1414 : $(this.gui_objects.qsearchbox).val(old.search);
  1415. 1415 : this.env.search_request = old.search_request;
  1416. 1416 : this.list_contacts_remote(null, null, this.env.current_page = old.page);
  1417. 1417 : }
  1418. 1418 : else
  1419. 1419 : this.list_contacts(props.source, this.env.address_group_stack[this.env.address_group_stack.length-1].id);
  1420. 1420 : }
  1421. 1421 : break;
  1422. 1422 :
  1423. 1423 : case 'import-messages':
  1424. 1424 : var form = props || this.gui_objects.importform,
  1425. 1425 : importlock = this.set_busy(true, 'importwait');
  1426. 1426 :
  1427. 1427 : if (!(flag = this.upload_file(form, 'import', importlock))) {
  1428. 1428 : this.set_busy(false, null, importlock);
  1429. 1429 : if (flag !== false)
  1430. 1430 : this.alert_dialog(this.get_label('selectimportfile'));
  1431. 1431 : this.command_aborted = true;
  1432. 1432 : }
  1433. 1433 : break;
  1434. 1434 :
  1435. 1435 : case 'import':
  1436. 1436 : var dialog = $('<iframe>').attr('src', this.url('import', {_framed: 1, _target: this.env.source})),
  1437. 1437 : import_func = function(e) {
  1438. 1438 : var win = dialog[0].contentWindow,
  1439. 1439 : form = null;
  1440. 1440 :
  1441. 1441 : if (win.rcmail.gui_objects.importformmap)
  1442. 1442 : form = win.rcmail.gui_objects.importformmap;
  1443. 1443 : else
  1444. 1444 : form = win.rcmail.gui_objects.importform;
  1445. 1445 :
  1446. 1446 : if (form) {
  1447. 1447 : var lock, file = win.$('#rcmimportfile')[0];
  1448. 1448 : if (file && !file.value) {
  1449. 1449 : win.rcmail.alert_dialog(win.rcmail.get_label('selectimportfile'));
  1450. 1450 : return;
  1451. 1451 : }
  1452. 1452 :
  1453. 1453 : lock = win.rcmail.set_busy(true, 'importwait');
  1454. 1454 : $('[name="_unlock"]', form).val(lock);
  1455. 1455 : form.submit();
  1456. 1456 : win.rcmail.lock_form(form, true);
  1457. 1457 : // disable Import button
  1458. 1458 : $(e.target).attr('disabled', true).next().focus();
  1459. 1459 : }
  1460. 1460 : },
  1461. 1461 : close_func = function(event, ui) {
  1462. 1462 : $(this).remove();
  1463. 1463 : if (ref.import_state == 'reload')
  1464. 1464 : ref.command('list');
  1465. 1465 : };
  1466. 1466 :
  1467. 1467 : this.import_state = null;
  1468. 1468 : this.import_dialog = this.simple_dialog(dialog, this.gettext('importcontacts'), import_func, {
  1469. 1469 : close: close_func,
  1470. 1470 : button: 'import',
  1471. 1471 : width: 500,
  1472. 1472 : height: 300
  1473. 1473 : });
  1474. 1474 :
  1475. 1475 : break;
  1476. 1476 :
  1477. 1477 : case 'export':
  1478. 1478 : if (this.contact_list.rowcount > 0) {
  1479. 1479 : this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request }, false, true);
  1480. 1480 : }
  1481. 1481 : break;
  1482. 1482 :
  1483. 1483 : case 'export-selected':
  1484. 1484 : if (this.contact_list.rowcount > 0) {
  1485. 1485 : this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') }, false, true);
  1486. 1486 : }
  1487. 1487 : break;
  1488. 1488 :
  1489. 1489 : case 'upload-photo':
  1490. 1490 : this.upload_contact_photo(props || this.gui_objects.uploadform);
  1491. 1491 : break;
  1492. 1492 :
  1493. 1493 : case 'delete-photo':
  1494. 1494 : this.replace_contact_photo('-del-');
  1495. 1495 : break;
  1496. 1496 :
  1497. 1497 : case 'undo':
  1498. 1498 : this.http_request('undo', '', this.display_message('', 'loading'));
  1499. 1499 : break;
  1500. 1500 :
  1501. 1501 : // unified command call (command name == function name)
  1502. 1502 : default:
  1503. 1503 : var func = command.replace(/-/g, '_');
  1504. 1504 : if (this[func] && typeof this[func] === 'function') {
  1505. 1505 : return this[func](props, obj, event);
  1506. 1506 : }
  1507. 1507 : }
  1508. 1508 : };
  1509. 1509 :
  1510. 1510 : // set command(s) enabled or disabled
  1511. 1511 : this.enable_command = function()
  1512. 1512 : {
  1513. 1513 : var i, n, args = Array.prototype.slice.call(arguments),
  1514. 1514 : enable = args.pop(), cmd;
  1515. 1515 :
  1516. 1516 : for (n=0; n<args.length; n++) {
  1517. 1517 : cmd = args[n];
  1518. 1518 : // argument of type array
  1519. 1519 : if (typeof cmd === 'string') {
  1520. 1520 : this.commands[cmd] = enable;
  1521. 1521 : this.set_button(cmd, (enable ? 'act' : 'pas'));
  1522. 1522 : this.triggerEvent('enable-command', {command: cmd, status: enable});
  1523. 1523 : }
  1524. 1524 : // push array elements into commands array
  1525. 1525 : else {
  1526. 1526 : for (i in cmd)
  1527. 1527 : args.push(cmd[i]);
  1528. 1528 : }
  1529. 1529 : }
  1530. 1530 :
  1531. 1531 : this.set_menu_buttons();
  1532. 1532 : };
  1533. 1533 :
  1534. 1534 : this.command_enabled = function(cmd)
  1535. 1535 : {
  1536. 1536 : return this.commands[cmd];
  1537. 1537 : };
  1538. 1538 :
  1539. 1539 : // lock/unlock interface
  1540. 1540 : this.set_busy = function(a, message, id)
  1541. 1541 : {
  1542. 1542 : if (a && message) {
  1543. 1543 : var msg = this.get_label(message);
  1544. 1544 : if (msg == message)
  1545. 1545 : msg = 'Loading...';
  1546. 1546 :
  1547. 1547 : id = this.display_message(msg, 'loading');
  1548. 1548 : }
  1549. 1549 : else if (!a && id) {
  1550. 1550 : this.hide_message(id);
  1551. 1551 : }
  1552. 1552 :
  1553. 1553 : this.busy = a;
  1554. 1554 :
  1555. 1555 : if (this.gui_objects.editform)
  1556. 1556 : this.lock_form(this.gui_objects.editform, a);
  1557. 1557 :
  1558. 1558 : return id;
  1559. 1559 : };
  1560. 1560 :
  1561. 1561 : // return a localized string
  1562. 1562 : this.get_label = function(name, domain)
  1563. 1563 : {
  1564. 1564 : if (domain && this.labels[domain+'.'+name])
  1565. 1565 : return this.labels[domain+'.'+name];
  1566. 1566 : else if (this.labels[name])
  1567. 1567 : return this.labels[name];
  1568. 1568 : else
  1569. 1569 : return name;
  1570. 1570 : };
  1571. 1571 :
  1572. 1572 : // alias for convenience reasons
  1573. 1573 : this.gettext = this.get_label;
  1574. 1574 :
  1575. 1575 : // switch to another application task
  1576. 1576 : this.switch_task = function(task)
  1577. 1577 : {
  1578. 1578 : var action, path;
  1579. 1579 :
  1580. 1580 : if ((path = task.split('/')).length == 2) {
  1581. 1581 : task = path[0];
  1582. 1582 : action = path[1];
  1583. 1583 : }
  1584. 1584 :
  1585. 1585 : if (this.task === task && task != 'mail')
  1586. 1586 : return;
  1587. 1587 :
  1588. 1588 : var url = this.get_task_url(task);
  1589. 1589 :
  1590. 1590 : if (action)
  1591. 1591 : url += '&_action=' + action;
  1592. 1592 :
  1593. 1593 : if (task == 'mail')
  1594. 1594 : url += '&_mbox=INBOX';
  1595. 1595 : else if (task == 'logout') {
  1596. 1596 : url = this.secure_url(url);
  1597. 1597 : this.clear_compose_data();
  1598. 1598 : }
  1599. 1599 :
  1600. 1600 : this.redirect(url);
  1601. 1601 : };
  1602. 1602 :
  1603. 1603 : this.get_task_url = function(task, url)
  1604. 1604 : {
  1605. 1605 : if (!url)
  1606. 1606 : url = this.env.comm_path;
  1607. 1607 :
  1608. 1608 : if (url.match(/[?&]_task=[a-zA-Z0-9_-]+/))
  1609. 1609 : return url.replace(/_task=[a-zA-Z0-9_-]+/, '_task=' + task);
  1610. 1610 : else
  1611. 1611 : return url.replace(/\?.*$/, '') + '?_task=' + task;
  1612. 1612 : };
  1613. 1613 :
  1614. 1614 : this.reload = function(delay)
  1615. 1615 : {
  1616. 1616 : if (this.is_framed())
  1617. 1617 : parent.rcmail.reload(delay);
  1618. 1618 : else if (delay)
  1619. 1619 : setTimeout(function() { ref.reload(); }, delay);
  1620. 1620 : else if (window.location)
  1621. 1621 : location.href = this.url('', {_extwin: this.env.extwin});
  1622. 1622 : };
  1623. 1623 :
  1624. 1624 : // Add variable to GET string, replace old value if exists
  1625. 1625 : this.add_url = function(url, name, value)
  1626. 1626 : {
  1627. 1627 : var urldata, datax, hash = '';
  1628. 1628 :
  1629. 1629 : value = urlencode(value);
  1630. 1630 :
  1631. 1631 : if (/(#[a-z0-9_-]*)$/.test(url)) {
  1632. 1632 : hash = RegExp.$1;
  1633. 1633 : url = url.substr(0, url.length - hash.length);
  1634. 1634 : }
  1635. 1635 :
  1636. 1636 : if (/(\?.*)$/.test(url)) {
  1637. 1637 : urldata = RegExp.$1;
  1638. 1638 : datax = RegExp('((\\?|&)'+RegExp.escape(name)+'=[^&]*)');
  1639. 1639 :
  1640. 1640 : if (datax.test(urldata))
  1641. 1641 : urldata = urldata.replace(datax, RegExp.$2 + name + '=' + value);
  1642. 1642 : else
  1643. 1643 : urldata += '&' + name + '=' + value;
  1644. 1644 :
  1645. 1645 : return url.replace(/(\?.*)$/, urldata) + hash;
  1646. 1646 : }
  1647. 1647 :
  1648. 1648 : return url + '?' + name + '=' + value + hash;
  1649. 1649 : };
  1650. 1650 :
  1651. 1651 : // append CSRF protection token to the given url
  1652. 1652 : this.secure_url = function(url)
  1653. 1653 : {
  1654. 1654 : return this.add_url(url, '_token', this.env.request_token);
  1655. 1655 : },
  1656. 1656 :
  1657. 1657 : this.is_framed = function()
  1658. 1658 : {
  1659. 1659 : return this.env.framed && parent.rcmail && parent.rcmail != this && typeof parent.rcmail.command == 'function';
  1660. 1660 : };
  1661. 1661 :
  1662. 1662 : this.save_pref = function(prop)
  1663. 1663 : {
  1664. 1664 : var request = {_name: prop.name, _value: prop.value};
  1665. 1665 :
  1666. 1666 : if (prop.session)
  1667. 1667 : request._session = prop.session;
  1668. 1668 : if (prop.env)
  1669. 1669 : this.env[prop.env] = prop.value;
  1670. 1670 :
  1671. 1671 : this.http_post('save-pref', request);
  1672. 1672 : };
  1673. 1673 :
  1674. 1674 : this.html_identifier = function(str, encode)
  1675. 1675 : {
  1676. 1676 : return encode ? this.html_identifier_encode(str) : String(str).replace(this.identifier_expr, '_');
  1677. 1677 : };
  1678. 1678 :
  1679. 1679 : this.html_identifier_encode = function(str)
  1680. 1680 : {
  1681. 1681 : return Base64.encode(String(str)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
  1682. 1682 : };
  1683. 1683 :
  1684. 1684 : this.html_identifier_decode = function(str)
  1685. 1685 : {
  1686. 1686 : str = String(str).replace(/-/g, '+').replace(/_/g, '/');
  1687. 1687 :
  1688. 1688 : while (str.length % 4) str += '=';
  1689. 1689 :
  1690. 1690 : return Base64.decode(str);
  1691. 1691 : };
  1692. 1692 :
  1693. 1693 :
  1694. 1694 : /*********************************************************/
  1695. 1695 : /********* event handling methods *********/
  1696. 1696 : /*********************************************************/
  1697. 1697 :
  1698. 1698 : this.drag_menu = function(e, target)
  1699. 1699 : {
  1700. 1700 : var modkey = rcube_event.get_modifier(e),
  1701. 1701 : menu = this.gui_objects.dragmenu;
  1702. 1702 :
  1703. 1703 : if (menu && modkey == SHIFT_KEY && this.commands['copy']) {
  1704. 1704 : var pos = rcube_event.get_mouse_pos(e);
  1705. 1705 : this.env.drag_target = target;
  1706. 1706 : this.show_menu(this.gui_objects.dragmenu.id, true, e);
  1707. 1707 : $(menu).css({top: (pos.y-10)+'px', left: (pos.x-10)+'px'});
  1708. 1708 : return true;
  1709. 1709 : }
  1710. 1710 :
  1711. 1711 : return false;
  1712. 1712 : };
  1713. 1713 :
  1714. 1714 : this.drag_menu_action = function(action)
  1715. 1715 : {
  1716. 1716 : var menu = this.gui_objects.dragmenu;
  1717. 1717 : if (menu) {
  1718. 1718 : $(menu).hide();
  1719. 1719 : }
  1720. 1720 :
  1721. 1721 : this.command(action, this.env.drag_target);
  1722. 1722 : this.env.drag_target = null;
  1723. 1723 : };
  1724. 1724 :
  1725. 1725 : this.drag_start = function(list)
  1726. 1726 : {
  1727. 1727 : this.drag_active = true;
  1728. 1728 :
  1729. 1729 : if (this.preview_timer)
  1730. 1730 : clearTimeout(this.preview_timer);
  1731. 1731 :
  1732. 1732 : // prepare treelist widget for dragging interactions
  1733. 1733 : if (this.treelist)
  1734. 1734 : this.treelist.drag_start();
  1735. 1735 : };
  1736. 1736 :
  1737. 1737 : this.drag_end = function(e)
  1738. 1738 : {
  1739. 1739 : var list, model;
  1740. 1740 :
  1741. 1741 : if (this.treelist)
  1742. 1742 : this.treelist.drag_end();
  1743. 1743 :
  1744. 1744 : // execute drag & drop action when mouse was released
  1745. 1745 : if (list = this.message_list)
  1746. 1746 : model = this.env.mailboxes;
  1747. 1747 : else if (list = this.contact_list)
  1748. 1748 : model = this.env.contactfolders;
  1749. 1749 :
  1750. 1750 : // Note: we accept only mouse events to ignore dragging aborts with ESC key (#6623)
  1751. 1751 : if (this.drag_active && model && this.env.last_folder_target && !rcube_event.is_keyboard(e)) {
  1752. 1752 : var target = model[this.env.last_folder_target];
  1753. 1753 : list.draglayer.hide();
  1754. 1754 :
  1755. 1755 : if (this.contact_list) {
  1756. 1756 : if (!this.contacts_drag_menu(e, target))
  1757. 1757 : this.command('move', target);
  1758. 1758 : }
  1759. 1759 : else if (!this.drag_menu(e, target))
  1760. 1760 : this.command('move', target);
  1761. 1761 : }
  1762. 1762 :
  1763. 1763 : this.drag_active = false;
  1764. 1764 : this.env.last_folder_target = null;
  1765. 1765 : };
  1766. 1766 :
  1767. 1767 : this.drag_move = function(e)
  1768. 1768 : {
  1769. 1769 : if (this.gui_objects.folderlist) {
  1770. 1770 : var drag_target, oldclass,
  1771. 1771 : layerclass = 'draglayernormal',
  1772. 1772 : mouse = rcube_event.get_mouse_pos(e);
  1773. 1773 :
  1774. 1774 : if (this.contact_list && this.contact_list.draglayer)
  1775. 1775 : oldclass = this.contact_list.draglayer.attr('class');
  1776. 1776 :
  1777. 1777 : // mouse intersects a valid drop target on the treelist
  1778. 1778 : if (this.treelist && (drag_target = this.treelist.intersects(mouse, true))) {
  1779. 1779 : this.env.last_folder_target = drag_target;
  1780. 1780 : layerclass = 'draglayer' + (this.check_droptarget(drag_target) > 1 ? 'copy' : 'normal');
  1781. 1781 : }
  1782. 1782 : else {
  1783. 1783 : // Clear target, otherwise drag end will trigger move into last valid droptarget
  1784. 1784 : this.env.last_folder_target = null;
  1785. 1785 : }
  1786. 1786 :
  1787. 1787 : if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
  1788. 1788 : this.contact_list.draglayer.attr('class', layerclass);
  1789. 1789 : }
  1790. 1790 : };
  1791. 1791 :
  1792. 1792 : this.collapse_folder = function(name)
  1793. 1793 : {
  1794. 1794 : if (this.treelist)
  1795. 1795 : this.treelist.toggle(name);
  1796. 1796 : };
  1797. 1797 :
  1798. 1798 : this.folder_collapsed = function(node)
  1799. 1799 : {
  1800. 1800 : if (this.folder_collapsed_timer)
  1801. 1801 : clearTimeout(this.folder_collapsed_timer);
  1802. 1802 :
  1803. 1803 : var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders',
  1804. 1804 : old = this.env[prefname];
  1805. 1805 :
  1806. 1806 : if (node.collapsed) {
  1807. 1807 : this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&';
  1808. 1808 :
  1809. 1809 : // select the folder if one of its children is currently selected
  1810. 1810 : // don't select if it's virtual (#1488346)
  1811. 1811 : if (!node.virtual && this.env.mailbox && this.env.mailbox.startsWith(node.id + this.env.delimiter))
  1812. 1812 : this.command('list', node.id);
  1813. 1813 : }
  1814. 1814 : else {
  1815. 1815 : var reg = new RegExp('&'+urlencode(node.id)+'&');
  1816. 1816 : this.env[prefname] = this.env[prefname].replace(reg, '');
  1817. 1817 : }
  1818. 1818 :
  1819. 1819 : if (!this.drag_active) {
  1820. 1820 : if (old !== this.env[prefname])
  1821. 1821 : this.folder_collapsed_timer = setTimeout(function() { ref.command('save-pref', { name: prefname, value: ref.env[prefname] }); }, 10);
  1822. 1822 :
  1823. 1823 : if (this.env.unread_counts)
  1824. 1824 : this.set_unread_count_display(node.id, false);
  1825. 1825 : }
  1826. 1826 : };
  1827. 1827 :
  1828. 1828 : // global mouse-click handler to cleanup some UI elements
  1829. 1829 : this.doc_mouse_up = function(e)
  1830. 1830 : {
  1831. 1831 : var list, id, target = rcube_event.get_target(e);
  1832. 1832 :
  1833. 1833 : // ignore event if jquery UI dialog is open
  1834. 1834 : if ($(target).closest('.ui-dialog, .ui-widget-overlay').length)
  1835. 1835 : return;
  1836. 1836 :
  1837. 1837 : // remove focus from list widgets
  1838. 1838 : if (window.rcube_list_widget && rcube_list_widget._instances.length) {
  1839. 1839 : $.each(rcube_list_widget._instances, function(i,list) {
  1840. 1840 : if (list && !rcube_mouse_is_over(e, list.list.parentNode))
  1841. 1841 : list.blur();
  1842. 1842 : });
  1843. 1843 : }
  1844. 1844 :
  1845. 1845 : // reset 'pressed' buttons
  1846. 1846 : if (this.buttons_sel) {
  1847. 1847 : for (id in this.buttons_sel)
  1848. 1848 : if (typeof id !== 'function')
  1849. 1849 : this.button_out(this.buttons_sel[id], id);
  1850. 1850 : this.buttons_sel = {};
  1851. 1851 : }
  1852. 1852 :
  1853. 1853 : // reset popup menus; delayed to have updated menu_stack data
  1854. 1854 : setTimeout(function(e) {
  1855. 1855 : var obj, skip, config, id, i, parents = $(target).parents();
  1856. 1856 : for (i = ref.menu_stack.length - 1; i >= 0; i--) {
  1857. 1857 : id = ref.menu_stack[i];
  1858. 1858 : obj = $('#' + id);
  1859. 1859 :
  1860. 1860 : if (obj.is(':visible')
  1861. 1861 : && target != obj.data('opener')
  1862. 1862 : && target != obj.get(0) // check if scroll bar was clicked (#1489832)
  1863. 1863 : && !parents.is(obj.data('opener'))
  1864. 1864 : && id != skip
  1865. 1865 : && (obj.attr('data-editable') != 'true' || !$(target).parents('#' + id).length)
  1866. 1866 : && (obj.attr('data-sticky') != 'true' || !rcube_mouse_is_over(e, obj.get(0)))
  1867. 1867 : ) {
  1868. 1868 : ref.hide_menu(id, e);
  1869. 1869 : }
  1870. 1870 : skip = obj.data('parent');
  1871. 1871 : }
  1872. 1872 : }, 10, e);
  1873. 1873 : };
  1874. 1874 :
  1875. 1875 : // global keypress event handler
  1876. 1876 : this.doc_keypress = function(e)
  1877. 1877 : {
  1878. 1878 : // Helper method to move focus to the next/prev active menu item
  1879. 1879 : var focus_menu_item = function(dir) {
  1880. 1880 : var obj, item, mod = dir < 0 ? 'prevAll' : 'nextAll', limit = dir < 0 ? 'last' : 'first';
  1881. 1881 : if (ref.focused_menu && (obj = $('#'+ref.focused_menu))) {
  1882. 1882 : item = obj.find(':focus').closest('li')[mod]().has(':not([aria-disabled=true])').find('a,input')[limit]();
  1883. 1883 : if (!item.length)
  1884. 1884 : item = obj.find(':focus').closest('ul')[mod]().has(':not([aria-disabled=true])').find('a,input')[limit]();
  1885. 1885 : return item.focus().length;
  1886. 1886 : }
  1887. 1887 :
  1888. 1888 : return 0;
  1889. 1889 : };
  1890. 1890 :
  1891. 1891 : var target = e.target || {},
  1892. 1892 : keyCode = rcube_event.get_keycode(e);
  1893. 1893 :
  1894. 1894 : if (e.keyCode != 27 && (!this.menu_keyboard_active || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')) {
  1895. 1895 : return true;
  1896. 1896 : }
  1897. 1897 :
  1898. 1898 : switch (keyCode) {
  1899. 1899 : case 38:
  1900. 1900 : case 40:
  1901. 1901 : case 63232: // "up", in safari keypress
  1902. 1902 : case 63233: // "down", in safari keypress
  1903. 1903 : focus_menu_item(keyCode == 38 || keyCode == 63232 ? -1 : 1);
  1904. 1904 : return rcube_event.cancel(e);
  1905. 1905 :
  1906. 1906 : case 9: // tab
  1907. 1907 : if (this.focused_menu) {
  1908. 1908 : var mod = rcube_event.get_modifier(e);
  1909. 1909 : if (!focus_menu_item(mod == SHIFT_KEY ? -1 : 1)) {
  1910. 1910 : this.hide_menu(this.focused_menu, e);
  1911. 1911 : }
  1912. 1912 : }
  1913. 1913 : return rcube_event.cancel(e);
  1914. 1914 :
  1915. 1915 : case 27: // esc
  1916. 1916 : if (this.menu_stack.length)
  1917. 1917 : this.hide_menu(this.menu_stack[this.menu_stack.length-1], e);
  1918. 1918 : break;
  1919. 1919 : }
  1920. 1920 :
  1921. 1921 : return true;
  1922. 1922 : }
  1923. 1923 :
  1924. 1924 : // Common handler for a keypress event on a list widget
  1925. 1925 : this.list_keypress = function(list, conf)
  1926. 1926 : {
  1927. 1927 : if (list.modkey == CONTROL_KEY)
  1928. 1928 : return;
  1929. 1929 :
  1930. 1930 : if (list.key_pressed == list.DELETE_KEY || list.key_pressed == list.BACKSPACE_KEY)
  1931. 1931 : this.command(conf && conf.del ? conf.del : 'delete');
  1932. 1932 : else if (list.key_pressed == 33)
  1933. 1933 : this.command(conf && conf.prev ? conf.prev : 'previouspage');
  1934. 1934 : else if (list.key_pressed == 34)
  1935. 1935 : this.command(conf && conf.next ? conf.next : 'nextpage');
  1936. 1936 : };
  1937. 1937 :
  1938. 1938 : // Handler for a keypress event on a messages list widget
  1939. 1939 : this.msglist_keypress = function(list)
  1940. 1940 : {
  1941. 1941 : // On Enter open the message in list layout mode (no preview frame)
  1942. 1942 : if (list.key_pressed == list.ENTER_KEY && !this.env.contentframe)
  1943. 1943 : this.command('show');
  1944. 1944 : else
  1945. 1945 : this.list_keypress(list);
  1946. 1946 : };
  1947. 1947 :
  1948. 1948 : this.msglist_select = function(list)
  1949. 1949 : {
  1950. 1950 : if (this.preview_timer)
  1951. 1951 : clearTimeout(this.preview_timer);
  1952. 1952 :
  1953. 1953 : var isDraft = false,
  1954. 1954 : selected = list.get_single_selection(),
  1955. 1955 : selection = list.get_selection(false),
  1956. 1956 : selected_count = selection.length;
  1957. 1957 :
  1958. 1958 : this.enable_command(this.env.message_commands, selected != null);
  1959. 1959 :
  1960. 1960 : // Find out whether any of the selected messages comes from the Drafts folder
  1961. 1961 : if (selected_count > 0) {
  1962. 1962 : if (!this.env.multifolder_listing) {
  1963. 1963 : isDraft = this.env.mailbox == this.env.drafts_mailbox
  1964. 1964 : }
  1965. 1965 : else {
  1966. 1966 : $.each(selection, function(i, v) {
  1967. 1967 : if (ref.get_message_mailbox(v) == ref.env.drafts_mailbox) {
  1968. 1968 : isDraft = true;
  1969. 1969 : return false;
  1970. 1970 : }
  1971. 1971 : });
  1972. 1972 : }
  1973. 1973 : }
  1974. 1974 :
  1975. 1975 : // Disable some actions enabled above
  1976. 1976 : if (selected) {
  1977. 1977 : if (isDraft) {
  1978. 1978 : this.enable_command('reply', 'reply-all', 'reply-list', 'forward', 'forward-inline',
  1979. 1979 : 'forward-attachment', 'bounce', false);
  1980. 1980 : }
  1981. 1981 : else {
  1982. 1982 : var msg = this.env.messages[selected];
  1983. 1983 : if (!msg.ml)
  1984. 1984 : this.enable_command('reply-list', false);
  1985. 1985 : }
  1986. 1986 : }
  1987. 1987 :
  1988. 1988 : // Multi-message commands
  1989. 1989 : this.enable_command('delete', 'move', 'copy', 'mark', selected_count > 0);
  1990. 1990 : this.enable_command('forward', 'forward-attachment', !isDraft && selected_count > 0);
  1991. 1991 :
  1992. 1992 : // reset all-pages-selection
  1993. 1993 : if (selected || (selected_count && selected_count != list.rowcount))
  1994. 1994 : this.select_all_mode = false;
  1995. 1995 :
  1996. 1996 : // start timer for message preview (wait for double click)
  1997. 1997 : if (selected && this.env.contentframe && !list.multi_selecting && !this.dummy_select) {
  1998. 1998 : // try to be responsive and try not to overload the server when user is pressing up/down key repeatedly
  1999. 1999 : var now = new Date().getTime(),
  2000. 2000 : time_diff = now - (this._last_msglist_select_time || 0),
  2001. 2001 : preview_pane_delay = this.preview_delay_click;
  2002. 2002 :
  2003. 2003 : // user is selecting messages repeatedly, wait until this ends (use larger delay)
  2004. 2004 : if (time_diff < this.preview_delay_select) {
  2005. 2005 : preview_pane_delay = this.preview_delay_select;
  2006. 2006 : if (this.preview_timer) {
  2007. 2007 : clearTimeout(this.preview_timer);
  2008. 2008 : }
  2009. 2009 : if (this.env.contentframe) {
  2010. 2010 : this.show_contentframe(false);
  2011. 2011 : }
  2012. 2012 : }
  2013. 2013 :
  2014. 2014 : this._last_msglist_select_time = now;
  2015. 2015 : this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, preview_pane_delay);
  2016. 2016 : }
  2017. 2017 : else if (this.env.contentframe) {
  2018. 2018 : this.show_contentframe(false);
  2019. 2019 : }
  2020. 2020 : };
  2021. 2021 :
  2022. 2022 : this.msglist_dbl_click = function(list)
  2023. 2023 : {
  2024. 2024 : if (this.preview_timer)
  2025. 2025 : clearTimeout(this.preview_timer);
  2026. 2026 :
  2027. 2027 : var mbox, uid = list.get_single_selection();
  2028. 2028 :
  2029. 2029 : // TODO: Here we should just use this.command('show') but we can't
  2030. 2030 : // because at this point this.busy=true (set by msglist_get_preview())
  2031. 2031 :
  2032. 2032 : if (uid) {
  2033. 2033 : mbox = this.get_message_mailbox(uid);
  2034. 2034 : if (mbox == this.env.drafts_mailbox)
  2035. 2035 : this.open_compose_step({_draft_uid: uid, _mbox: mbox});
  2036. 2036 : else
  2037. 2037 : this.show_message(uid);
  2038. 2038 : }
  2039. 2039 : };
  2040. 2040 :
  2041. 2041 : this.msglist_get_preview = function()
  2042. 2042 : {
  2043. 2043 : var uid = this.get_single_uid();
  2044. 2044 : if (uid && this.env.contentframe && !this.drag_active)
  2045. 2045 : this.show_message(uid, false, true);
  2046. 2046 : else if (this.env.contentframe)
  2047. 2047 : this.show_contentframe(false);
  2048. 2048 : };
  2049. 2049 :
  2050. 2050 : this.msglist_expand = function(row)
  2051. 2051 : {
  2052. 2052 : if (this.env.messages[row.uid])
  2053. 2053 : this.env.messages[row.uid].expanded = row.expanded;
  2054. 2054 : $(row.obj)[row.expanded?'addClass':'removeClass']('expanded');
  2055. 2055 : };
  2056. 2056 :
  2057. 2057 : this.msglist_set_coltypes = function(list)
  2058. 2058 : {
  2059. 2059 : var i, found, name, cols = list.thead.rows[0].cells;
  2060. 2060 :
  2061. 2061 : this.env.listcols = [];
  2062. 2062 :
  2063. 2063 : for (i=0; i<cols.length; i++)
  2064. 2064 : if (cols[i].id && cols[i].id.startsWith('rcm')) {
  2065. 2065 : name = cols[i].id.slice(3);
  2066. 2066 : this.env.listcols.push(name);
  2067. 2067 : }
  2068. 2068 :
  2069. 2069 : // update message list setup
  2070. 2070 : this.msglist_setup(this.env.layout);
  2071. 2071 :
  2072. 2072 : if ((found = $.inArray('flag', this.env.listcols)) >= 0)
  2073. 2073 : this.env.flagged_col = found;
  2074. 2074 :
  2075. 2075 : if ((found = $.inArray('subject', this.env.listcols)) >= 0)
  2076. 2076 : this.env.subject_col = found;
  2077. 2077 :
  2078. 2078 : this.command('save-pref', { name: 'list_cols', value: this.env.listcols, session: 'list_attrib/columns' });
  2079. 2079 : };
  2080. 2080 :
  2081. 2081 : this.msglist_setup = function(layout)
  2082. 2082 : {
  2083. 2083 : var ret, listcols;
  2084. 2084 :
  2085. 2085 : // allow plugins or skins to override default list layout
  2086. 2086 : if (ret = this.triggerEvent('msglist_layout', layout))
  2087. 2087 : layout = ret;
  2088. 2088 :
  2089. 2089 : listcols = this.env[layout == 'widescreen' ? 'listcols_widescreen' : 'listcols'];
  2090. 2090 :
  2091. 2091 : if (layout == 'widescreen' && !this.env.threading)
  2092. 2092 : listcols = $.grep(listcols, function(value) { return value != 'threads'; });
  2093. 2093 :
  2094. 2094 : // set env vars for message list
  2095. 2095 : this.env.msglist_layout = layout;
  2096. 2096 : this.env.msglist_cols = listcols;
  2097. 2097 :
  2098. 2098 : // Set sort-* class on the list element
  2099. 2099 : var list = this.gui_objects.messagelist,
  2100. 2100 : classes = list.className.split(' ').filter(function(v) { return !v.startsWith('sort-'); });
  2101. 2101 :
  2102. 2102 : classes.push('sort-' + (this.env.sort_col || 'none'));
  2103. 2103 : list.className = classes.join(' ');
  2104. 2104 : };
  2105. 2105 :
  2106. 2106 : this.check_droptarget = function(id)
  2107. 2107 : {
  2108. 2108 : switch (this.task) {
  2109. 2109 : case 'mail':
  2110. 2110 : return (this.env.mailboxes[id]
  2111. 2111 : && !this.env.mailboxes[id].virtual
  2112. 2112 : && (this.env.mailboxes[id].id != this.env.mailbox || this.is_multifolder_listing())) ? 1 : 0;
  2113. 2113 :
  2114. 2114 : case 'addressbook':
  2115. 2115 : var target;
  2116. 2116 : if (id != this.env.source && (target = this.env.contactfolders[id])) {
  2117. 2117 : // droptarget is a group
  2118. 2118 : if (target.type == 'group') {
  2119. 2119 : if (target.id != this.env.group && !this.env.contactfolders[target.source].readonly) {
  2120. 2120 : var is_other = this.env.selection_sources.length > 1 || $.inArray(target.source, this.env.selection_sources) == -1;
  2121. 2121 : return !is_other || this.commands.move ? 1 : 2;
  2122. 2122 : }
  2123. 2123 : }
  2124. 2124 : // droptarget is a (writable) addressbook and it's not the source
  2125. 2125 : else if (!target.readonly && (this.env.selection_sources.length > 1 || $.inArray(id, this.env.selection_sources) == -1)) {
  2126. 2126 : return this.commands.move ? 1 : 2;
  2127. 2127 : }
  2128. 2128 : }
  2129. 2129 : }
  2130. 2130 :
  2131. 2131 : return 0;
  2132. 2132 : };
  2133. 2133 :
  2134. 2134 : // open popup window
  2135. 2135 : this.open_window = function(url, small, toolbar)
  2136. 2136 : {
  2137. 2137 : var wname = 'rcmextwin' + new Date().getTime();
  2138. 2138 :
  2139. 2139 : url += (url.match(/\?/) ? '&' : '?') + '_extwin=1';
  2140. 2140 :
  2141. 2141 : if (this.env.standard_windows)
  2142. 2142 : var extwin = window.open(url, wname);
  2143. 2143 : else {
  2144. 2144 : var win = this.is_framed() ? parent.window : window,
  2145. 2145 : page = $(win),
  2146. 2146 : page_width = page.width(),
  2147. 2147 : page_height = bw.mz ? $('body', win).height() : page.height(),
  2148. 2148 : w = Math.min(small ? this.env.popup_width_small : this.env.popup_width, page_width),
  2149. 2149 : h = page_height, // always use same height
  2150. 2150 : l = (win.screenLeft || win.screenX) + 20,
  2151. 2151 : t = (win.screenTop || win.screenY) + 20,
  2152. 2152 : extwin = window.open(url, wname,
  2153. 2153 : 'width='+w+',height='+h+',top='+t+',left='+l+',resizable=yes,location=no,scrollbars=yes'
  2154. 2154 : +(toolbar ? ',toolbar=yes,menubar=yes,status=yes' : ',toolbar=no,menubar=no,status=no'));
  2155. 2155 : }
  2156. 2156 :
  2157. 2157 : // detect popup blocker (#1489618)
  2158. 2158 : // don't care this might not work with all browsers
  2159. 2159 : if (!extwin || extwin.closed) {
  2160. 2160 : this.display_message('windowopenerror', 'warning');
  2161. 2161 : return;
  2162. 2162 : }
  2163. 2163 :
  2164. 2164 : // write loading... message to empty windows
  2165. 2165 : if (!url && extwin.document) {
  2166. 2166 : extwin.document.write('<html><body>' + this.get_label('loading') + '</body></html>');
  2167. 2167 : }
  2168. 2168 :
  2169. 2169 : // allow plugins to grab the window reference (#1489413)
  2170. 2170 : this.triggerEvent('openwindow', { url:url, handle:extwin });
  2171. 2171 :
  2172. 2172 : // focus window, delayed to bring to front
  2173. 2173 : setTimeout(function() { extwin && extwin.focus(); }, 10);
  2174. 2174 :
  2175. 2175 : return extwin;
  2176. 2176 : };
  2177. 2177 :
  2178. 2178 :
  2179. 2179 : /*********************************************************/
  2180. 2180 : /********* (message) list functionality *********/
  2181. 2181 : /*********************************************************/
  2182. 2182 :
  2183. 2183 : this.init_message_row = function(row)
  2184. 2184 : {
  2185. 2185 : var i, fn = {}, uid = row.uid,
  2186. 2186 : status_icon = (this.env.status_col != null ? 'status' : 'msg') + 'icn' + row.id;
  2187. 2187 :
  2188. 2188 : if (uid && this.env.messages[uid])
  2189. 2189 : $.extend(row, this.env.messages[uid]);
  2190. 2190 :
  2191. 2191 : // set eventhandler to status icon
  2192. 2192 : if (row.icon = document.getElementById(status_icon)) {
  2193. 2193 : fn.icon = function(e) { ref.command('toggle_status', uid); };
  2194. 2194 : }
  2195. 2195 :
  2196. 2196 : // save message icon position too
  2197. 2197 : if (this.env.status_col != null)
  2198. 2198 : row.msgicon = document.getElementById('msgicn'+row.id);
  2199. 2199 : else
  2200. 2200 : row.msgicon = row.icon;
  2201. 2201 :
  2202. 2202 : // set eventhandler to flag icon
  2203. 2203 : if (this.env.flagged_col != null && (row.flagicon = document.getElementById('flagicn'+row.id))) {
  2204. 2204 : fn.flagicon = function(e) { ref.command('toggle_flag', uid); };
  2205. 2205 : }
  2206. 2206 :
  2207. 2207 : // set event handler to thread expand/collapse icon
  2208. 2208 : if (!row.depth && row.has_children && (row.expando = document.getElementById('rcmexpando'+row.id))) {
  2209. 2209 : fn.expando = function(e) { ref.expand_message_row(e, uid); };
  2210. 2210 : }
  2211. 2211 :
  2212. 2212 : // attach events
  2213. 2213 : $.each(fn, function(i, f) {
  2214. 2214 : row[i].onclick = function(e) { f(e); return rcube_event.cancel(e); };
  2215. 2215 : if (bw.touch && row[i].addEventListener) {
  2216. 2216 : row[i].addEventListener('touchend', function(e) {
  2217. 2217 : if (e.changedTouches.length == 1) {
  2218. 2218 : f(e);
  2219. 2219 : return rcube_event.cancel(e);
  2220. 2220 : }
  2221. 2221 : }, false);
  2222. 2222 : }
  2223. 2223 : });
  2224. 2224 :
  2225. 2225 : this.triggerEvent('insertrow', { uid:uid, row:row });
  2226. 2226 : };
  2227. 2227 :
  2228. 2228 : // create a table row in the message list
  2229. 2229 : this.add_message_row = function(uid, cols, flags, attop)
  2230. 2230 : {
  2231. 2231 : if (!this.gui_objects.messagelist || !this.message_list)
  2232. 2232 : return false;
  2233. 2233 :
  2234. 2234 : // Prevent from adding messages from different folder (#1487752)
  2235. 2235 : if (flags.mbox != this.env.mailbox && !flags.skip_mbox_check)
  2236. 2236 : return false;
  2237. 2237 :
  2238. 2238 : // When deleting messages fast it may happen that the same message
  2239. 2239 : // from the next page could be added many times, we prevent this here
  2240. 2240 : if (this.message_list.rows[uid])
  2241. 2241 : return false;
  2242. 2242 :
  2243. 2243 : if (!this.env.messages[uid])
  2244. 2244 : this.env.messages[uid] = {};
  2245. 2245 :
  2246. 2246 : // merge flags over local message object
  2247. 2247 : $.extend(this.env.messages[uid], {
  2248. 2248 : deleted: flags.deleted?1:0,
  2249. 2249 : replied: flags.answered?1:0,
  2250. 2250 : unread: !flags.seen?1:0,
  2251. 2251 : forwarded: flags.forwarded?1:0,
  2252. 2252 : flagged: flags.flagged?1:0,
  2253. 2253 : has_children: flags.has_children?1:0,
  2254. 2254 : depth: flags.depth?flags.depth:0,
  2255. 2255 : unread_children: flags.unread_children || 0,
  2256. 2256 : flagged_children: flags.flagged_children || 0,
  2257. 2257 : parent_uid: flags.parent_uid || 0,
  2258. 2258 : selected: this.select_all_mode || this.message_list.in_selection(uid),
  2259. 2259 : ml: flags.ml?1:0,
  2260. 2260 : ctype: flags.ctype,
  2261. 2261 : mbox: flags.mbox,
  2262. 2262 : // flags from plugins
  2263. 2263 : flags: flags.extra_flags
  2264. 2264 : });
  2265. 2265 :
  2266. 2266 : var c, n, col, html, css_class, label, status_class = '', status_label = '',
  2267. 2267 : tree = '', expando = '',
  2268. 2268 : list = this.message_list,
  2269. 2269 : rows = list.rows,
  2270. 2270 : message = this.env.messages[uid],
  2271. 2271 : msg_id = this.html_identifier(uid,true),
  2272. 2272 : row_class = 'message'
  2273. 2273 : + (!flags.seen ? ' unread' : '')
  2274. 2274 : + (flags.deleted ? ' deleted' : '')
  2275. 2275 : + (flags.flagged ? ' flagged' : '')
  2276. 2276 : + (message.selected ? ' selected' : ''),
  2277. 2277 : row = { cols:[], style:{}, id:'rcmrow'+msg_id, uid:uid },
  2278. 2278 : layout = this.env.msglist_layout,
  2279. 2279 : listcols = this.env.msglist_cols;
  2280. 2280 :
  2281. 2281 : // widescreen layout does not have a separate status column
  2282. 2282 : if (layout == 'widescreen')
  2283. 2283 : this.env.status_col = null;
  2284. 2284 : else if ((n = $.inArray('status', listcols)) >= 0)
  2285. 2285 : this.env.status_col = n;
  2286. 2286 :
  2287. 2287 : // message status icons
  2288. 2288 : css_class = 'msgicon';
  2289. 2289 : if (this.env.status_col === null) {
  2290. 2290 : css_class += ' status';
  2291. 2291 : if (flags.deleted) {
  2292. 2292 : status_class += ' deleted';
  2293. 2293 : status_label += this.get_label('deleted') + ' ';
  2294. 2294 : }
  2295. 2295 : else if (!flags.seen) {
  2296. 2296 : status_class += ' unread';
  2297. 2297 : status_label += this.get_label('unread') + ' ';
  2298. 2298 : }
  2299. 2299 : else if (flags.unread_children > 0) {
  2300. 2300 : status_class += ' unreadchildren';
  2301. 2301 : }
  2302. 2302 : }
  2303. 2303 : if (flags.answered) {
  2304. 2304 : status_class += ' replied';
  2305. 2305 : status_label += this.get_label('replied') + ' ';
  2306. 2306 : }
  2307. 2307 : if (flags.forwarded) {
  2308. 2308 : status_class += ' forwarded';
  2309. 2309 : status_label += this.get_label('forwarded') + ' ';
  2310. 2310 : }
  2311. 2311 :
  2312. 2312 : // update selection
  2313. 2313 : if (message.selected && !list.in_selection(uid))
  2314. 2314 : list.selection.push(uid);
  2315. 2315 :
  2316. 2316 : // threads
  2317. 2317 : if (this.env.threading) {
  2318. 2318 : if (message.depth) {
  2319. 2319 : // This assumes that div width is hardcoded to 15px,
  2320. 2320 : tree += '<span id="rcmtab' + msg_id + '" class="branch" style="width:' + (message.depth * 15) + 'px;">&nbsp;&nbsp;</span>';
  2321. 2321 :
  2322. 2322 : if ((rows[message.parent_uid] && rows[message.parent_uid].expanded === false)
  2323. 2323 : || ((this.env.autoexpand_threads == 0 || this.env.autoexpand_threads == 2) &&
  2324. 2324 : (!rows[message.parent_uid] || !rows[message.parent_uid].expanded))
  2325. 2325 : ) {
  2326. 2326 : row.style.display = 'none';
  2327. 2327 : message.expanded = false;
  2328. 2328 : }
  2329. 2329 : else
  2330. 2330 : message.expanded = true;
  2331. 2331 :
  2332. 2332 : row_class += ' thread expanded';
  2333. 2333 : }
  2334. 2334 : else if (message.has_children) {
  2335. 2335 : if (message.expanded === undefined && (this.env.autoexpand_threads == 1 || (this.env.autoexpand_threads == 2 && message.unread_children))) {
  2336. 2336 : message.expanded = true;
  2337. 2337 : }
  2338. 2338 :
  2339. 2339 : expando = '<div id="rcmexpando' + row.id + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '">&nbsp;&nbsp;</div>';
  2340. 2340 : row_class += ' thread' + (message.expanded ? ' expanded' : '');
  2341. 2341 : }
  2342. 2342 :
  2343. 2343 : if (flags.unread_children && flags.seen && !message.expanded)
  2344. 2344 : row_class += ' unroot';
  2345. 2345 :
  2346. 2346 : if (flags.flagged_children && !message.expanded)
  2347. 2347 : row_class += ' flaggedroot';
  2348. 2348 : }
  2349. 2349 :
  2350. 2350 : tree += '<span id="msgicn'+row.id+'" class="'+css_class+status_class+'" title="'+status_label+'"></span>';
  2351. 2351 : row.className = row_class;
  2352. 2352 :
  2353. 2353 : // build subject link
  2354. 2354 : if (cols.subject) {
  2355. 2355 : var action = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show',
  2356. 2356 : uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid',
  2357. 2357 : query = { _mbox: flags.mbox };
  2358. 2358 : query[uid_param] = uid;
  2359. 2359 : cols.subject = '<a href="' + this.url(action, query) + '" onclick="return rcube_event.keyboard_only(event)"' +
  2360. 2360 : ' onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')" tabindex="-1"><span>'+cols.subject+'</span></a>';
  2361. 2361 : }
  2362. 2362 :
  2363. 2363 : // add each submitted col
  2364. 2364 : for (n in listcols) {
  2365. 2365 : c = listcols[n];
  2366. 2366 : col = {className: String(c).toLowerCase(), events:{}};
  2367. 2367 :
  2368. 2368 : if (this.env.coltypes[c] && this.env.coltypes[c].hidden) {
  2369. 2369 : col.className += ' hidden';
  2370. 2370 : }
  2371. 2371 :
  2372. 2372 : if (c == 'flag') {
  2373. 2373 : css_class = (flags.flagged ? 'flagged' : 'unflagged');
  2374. 2374 : label = this.get_label(css_class);
  2375. 2375 : html = '<span id="flagicn'+row.id+'" class="'+css_class+'" title="'+label+'"></span>';
  2376. 2376 : }
  2377. 2377 : else if (c == 'attachment') {
  2378. 2378 : label = this.get_label('withattachment');
  2379. 2379 : if (flags.attachmentClass)
  2380. 2380 : html = '<span class="'+flags.attachmentClass+'" title="'+label+'"></span>';
  2381. 2381 : else if (flags.ctype == 'multipart/report')
  2382. 2382 : html = '<span class="report"></span>';
  2383. 2383 : else if (flags.ctype == 'multipart/encrypted' || flags.ctype == 'application/pkcs7-mime')
  2384. 2384 : html = '<span class="encrypted"></span>';
  2385. 2385 : else if (flags.hasattachment || (!flags.hasnoattachment && /application\/|multipart\/(m|signed)/.test(flags.ctype)))
  2386. 2386 : html = '<span class="attachment" title="'+label+'"></span>';
  2387. 2387 : else
  2388. 2388 : html = '&nbsp;';
  2389. 2389 : }
  2390. 2390 : else if (c == 'status') {
  2391. 2391 : label = '';
  2392. 2392 : if (flags.deleted) {
  2393. 2393 : css_class = 'deleted';
  2394. 2394 : label = this.get_label('deleted');
  2395. 2395 : }
  2396. 2396 : else if (!flags.seen) {
  2397. 2397 : css_class = 'unread';
  2398. 2398 : label = this.get_label('unread');
  2399. 2399 : }
  2400. 2400 : else if (flags.unread_children > 0) {
  2401. 2401 : css_class = 'unreadchildren';
  2402. 2402 : }
  2403. 2403 : else
  2404. 2404 : css_class = 'msgicon';
  2405. 2405 : html = '<span id="statusicn'+row.id+'" class="'+css_class+status_class+'" title="'+label+'"></span>';
  2406. 2406 : }
  2407. 2407 : else if (c == 'threads')
  2408. 2408 : html = expando;
  2409. 2409 : else if (c == "fromto")//PAMELA
  2410. 2410 : {
  2411. 2411 : html = tree + cols[c];
  2412. 2412 : }
  2413. 2413 : else if (c == 'subject') {
  2414. 2414 : html = cols[c];
  2415. 2415 : }
  2416. 2416 : else if (c == 'priority') {
  2417. 2417 : if (flags.prio > 0 && flags.prio < 6) {
  2418. 2418 : label = this.get_label('priority') + ' ' + flags.prio;
  2419. 2419 : html = '<span class="prio'+flags.prio+'" title="'+label+'"></span>';
  2420. 2420 : }
  2421. 2421 : else
  2422. 2422 : html = '&nbsp;';
  2423. 2423 : }
  2424. 2424 : else if (c == 'folder') {
  2425. 2425 : html = '<span onmouseover="rcube_webmail.long_subject_title(this)">' + cols[c] + '<span>';
  2426. 2426 : }
  2427. 2427 : else
  2428. 2428 : html = cols[c];
  2429. 2429 :
  2430. 2430 : // PAMELA
  2431. 2431 : col.innerHTML = rcmail.triggerEvent('rcmail.addrow.update_html', {html, uid, cols, flags, attop, c}) || html;;
  2432. 2432 : row.cols.push(col);
  2433. 2433 : }
  2434. 2434 :
  2435. 2435 : if (layout == 'widescreen')
  2436. 2436 : row = this.widescreen_message_row(row, uid, message);
  2437. 2437 :
  2438. 2438 : list.insert_row(row, attop);
  2439. 2439 :
  2440. 2440 : // remove 'old' row
  2441. 2441 : if (attop && this.env.pagesize && list.rowcount > this.env.pagesize) {
  2442. 2442 : var uid = list.get_last_row();
  2443. 2443 : list.remove_row(uid);
  2444. 2444 : list.clear_selection(uid);
  2445. 2445 : }
  2446. 2446 : };
  2447. 2447 :
  2448. 2448 : // Converts standard message list record into "widescreen" (3-column) layout
  2449. 2449 : this.widescreen_message_row = function(row, uid, message)
  2450. 2450 : {
  2451. 2451 : var domrow = document.createElement('tr');
  2452. 2452 :
  2453. 2453 : domrow.id = row.id;
  2454. 2454 : domrow.uid = row.uid;
  2455. 2455 : domrow.className = row.className;
  2456. 2456 : if (row.style) $.extend(domrow.style, row.style);
  2457. 2457 :
  2458. 2458 : $.each(this.env.widescreen_list_template, function() {
  2459. 2459 : if (!ref.env.threading && this.className == 'threads')
  2460. 2460 : return;
  2461. 2461 :
  2462. 2462 : var i, n, e, col, domcol,
  2463. 2463 : domcell = document.createElement('td');
  2464. 2464 :
  2465. 2465 : if (this.className) domcell.className = this.className;
  2466. 2466 :
  2467. 2467 : for (i=0; this.cells && i < this.cells.length; i++) {
  2468. 2468 : for (n=0; row.cols && n < row.cols.length; n++) {
  2469. 2469 : if (this.cells[i] == row.cols[n].className) {
  2470. 2470 : col = row.cols[n];
  2471. 2471 : domcol = document.createElement('span');
  2472. 2472 : domcol.className = this.cells[i];
  2473. 2473 : if (this.className == 'subject' && domcol.className != 'subject')
  2474. 2474 : domcol.className += ' skip-on-drag';
  2475. 2475 : if (col.innerHTML)
  2476. 2476 : domcol.innerHTML = col.innerHTML;
  2477. 2477 : domcell.appendChild(domcol);
  2478. 2478 : break;
  2479. 2479 : }
  2480. 2480 : }
  2481. 2481 : }
  2482. 2482 :
  2483. 2483 : domrow.appendChild(domcell);
  2484. 2484 : });
  2485. 2485 :
  2486. 2486 : if (this.env.threading && message.depth) {
  2487. 2487 : n = this.calculate_thread_padding(message.depth);
  2488. 2488 : $('td.subject', domrow).attr('style', 'padding-left:' + n + ' !important');
  2489. 2489 : $('span.branch', domrow).remove();
  2490. 2490 : }
  2491. 2491 :
  2492. 2492 : return domrow;
  2493. 2493 : };
  2494. 2494 :
  2495. 2495 : this.calculate_thread_padding = function(level)
  2496. 2496 : {
  2497. 2497 : ref.env.thread_padding.match(/^([0-9.]+)(.+)/);
  2498. 2498 : return (Math.min(6, level) * parseFloat(RegExp.$1)) + RegExp.$2;
  2499. 2499 : };
  2500. 2500 :
  2501. 2501 : this.set_list_sorting = function(sort_col, sort_order)
  2502. 2502 : {
  2503. 2503 : var sort_old = this.env.sort_col == 'arrival' ? 'date' : this.env.sort_col,
  2504. 2504 : sort_new = sort_col == 'arrival' ? 'date' : sort_col;
  2505. 2505 :
  2506. 2506 : // set table header class
  2507. 2507 : $('#rcm' + sort_old).removeClass('sorted' + this.env.sort_order.toUpperCase());
  2508. 2508 : if (sort_new)
  2509. 2509 : $('#rcm' + sort_new).addClass('sorted' + sort_order);
  2510. 2510 :
  2511. 2511 : // if sorting by 'arrival' is selected, click on date column should not switch to 'date'
  2512. 2512 : $('#rcmdate > a').prop('rel', sort_col == 'arrival' ? 'arrival' : 'date');
  2513. 2513 :
  2514. 2514 : this.env.sort_col = sort_col;
  2515. 2515 : this.env.sort_order = sort_order;
  2516. 2516 : };
  2517. 2517 :
  2518. 2518 : this.set_list_options = function(cols, sort_col, sort_order, threads, layout)
  2519. 2519 : {
  2520. 2520 : var update, post_data = {};
  2521. 2521 :
  2522. 2522 : if (sort_col === undefined)
  2523. 2523 : sort_col = this.env.sort_col;
  2524. 2524 : if (!sort_order)
  2525. 2525 : sort_order = this.env.sort_order;
  2526. 2526 :
  2527. 2527 : if (this.env.sort_col != sort_col || this.env.sort_order != sort_order) {
  2528. 2528 : update = 1;
  2529. 2529 : this.set_list_sorting(sort_col, sort_order);
  2530. 2530 : }
  2531. 2531 :
  2532. 2532 : if (this.env.threading != threads) {
  2533. 2533 : update = 1;
  2534. 2534 : post_data._threads = threads;
  2535. 2535 : }
  2536. 2536 :
  2537. 2537 : if (layout && this.env.layout != layout) {
  2538. 2538 : this.triggerEvent('layout-change', {old_layout: this.env.layout, new_layout: layout});
  2539. 2539 : update = 1;
  2540. 2540 : this.env.layout = post_data._layout = layout;
  2541. 2541 :
  2542. 2542 : // update message list setup
  2543. 2543 : this.msglist_setup(this.env.layout);
  2544. 2544 : }
  2545. 2545 :
  2546. 2546 : if (cols && cols.length) {
  2547. 2547 : // make sure new columns are added at the end of the list
  2548. 2548 : var i, idx, name, newcols = [], oldcols = this.env.listcols;
  2549. 2549 :
  2550. 2550 : for (i=0; i<oldcols.length; i++) {
  2551. 2551 : name = oldcols[i];
  2552. 2552 : idx = $.inArray(name, cols);
  2553. 2553 : if (idx != -1) {
  2554. 2554 : newcols.push(name);
  2555. 2555 : delete cols[idx];
  2556. 2556 : }
  2557. 2557 : }
  2558. 2558 : for (i=0; i<cols.length; i++)
  2559. 2559 : if (cols[i])
  2560. 2560 : newcols.push(cols[i]);
  2561. 2561 :
  2562. 2562 : if (newcols.join() != oldcols.join()) {
  2563. 2563 : update = 1;
  2564. 2564 : post_data._cols = newcols.join(',');
  2565. 2565 : }
  2566. 2566 : }
  2567. 2567 :
  2568. 2568 : if (update)
  2569. 2569 : this.list_mailbox('', '', sort_col+'_'+sort_order, post_data);
  2570. 2570 : };
  2571. 2571 :
  2572. 2572 : // when user double-clicks on a row
  2573. 2573 : this.show_message = function(id, safe, preview)
  2574. 2574 : {
  2575. 2575 : if (!id)
  2576. 2576 : return;
  2577. 2577 :
  2578. 2578 : var win, target = window,
  2579. 2579 : url = this.params_from_uid(id, {_caps: this.browser_capabilities()});
  2580. 2580 :
  2581. 2581 : if (preview && (win = this.get_frame_window(this.env.contentframe))) {
  2582. 2582 : target = win;
  2583. 2583 : url._framed = 1;
  2584. 2584 : }
  2585. 2585 :
  2586. 2586 : if (safe)
  2587. 2587 : url._safe = 1;
  2588. 2588 :
  2589. 2589 : // also send search request to get the right messages
  2590. 2590 : if (this.env.search_request)
  2591. 2591 : url._search = this.env.search_request;
  2592. 2592 :
  2593. 2593 : if (this.env.extwin)
  2594. 2594 : url._extwin = 1;
  2595. 2595 :
  2596. 2596 : url = this.url(preview ? 'preview': 'show', url);
  2597. 2597 :
  2598. 2598 : if (preview)
  2599. 2599 : this.preview_id = id;
  2600. 2600 :
  2601. 2601 : if (preview && String(target.location.href).indexOf(url) >= 0) {
  2602. 2602 : this.show_contentframe(true);
  2603. 2603 : }
  2604. 2604 : else {
  2605. 2605 : if (!preview && this.env.message_extwin && !this.env.extwin)
  2606. 2606 : this.open_window(url, true);
  2607. 2607 : else {
  2608. 2608 : // "Allow remote resources" reloads the page, we remove this request from the history,
  2609. 2609 : // so Back button works as expected, i.e. ignores the reload request (#6620)
  2610. 2610 : if (safe && document.referrer && window.history.replaceState)
  2611. 2611 : window.history.replaceState({}, '', document.referrer);
  2612. 2612 :
  2613. 2613 : this.location_href(url, target, true);
  2614. 2614 : }
  2615. 2615 : }
  2616. 2616 : };
  2617. 2617 :
  2618. 2618 : // update message status and unread counter after marking a message as read
  2619. 2619 : this.set_unread_message = function(id, folder)
  2620. 2620 : {
  2621. 2621 : var self = this;
  2622. 2622 :
  2623. 2623 : // find window with messages list
  2624. 2624 : if (!self.message_list)
  2625. 2625 : self = self.opener();
  2626. 2626 :
  2627. 2627 : if (!self && window.parent)
  2628. 2628 : self = parent.rcmail;
  2629. 2629 :
  2630. 2630 : if (!self || !self.message_list)
  2631. 2631 : return;
  2632. 2632 :
  2633. 2633 : // this may fail in multifolder mode
  2634. 2634 : if (self.set_message(id, 'unread', false) === false)
  2635. 2635 : self.set_message(id + '-' + folder, 'unread', false);
  2636. 2636 :
  2637. 2637 : if (self.env.unread_counts[folder] > 0) {
  2638. 2638 : self.env.unread_counts[folder] -= 1;
  2639. 2639 : self.set_unread_count(folder, self.env.unread_counts[folder], folder == 'INBOX' && !self.is_multifolder_listing());
  2640. 2640 : }
  2641. 2641 : };
  2642. 2642 :
  2643. 2643 : this.show_contentframe = function(show)
  2644. 2644 : {
  2645. 2645 : var frame, win, name = this.env.contentframe;
  2646. 2646 :
  2647. 2647 : if (frame = this.get_frame_element(name)) {
  2648. 2648 : if (!show && (win = this.get_frame_window(name))) {
  2649. 2649 : if (win.location.href.indexOf(this.env.blankpage) < 0) {
  2650. 2650 : if (win.stop)
  2651. 2651 : win.stop();
  2652. 2652 : else // IE
  2653. 2653 : win.document.execCommand('Stop');
  2654. 2654 :
  2655. 2655 : win.location.href = this.env.blankpage;
  2656. 2656 : }
  2657. 2657 : }
  2658. 2658 : else if (!bw.safari && !bw.konq)
  2659. 2659 : $(frame)[show ? 'show' : 'hide']();
  2660. 2660 : }
  2661. 2661 :
  2662. 2662 : if (!show) {
  2663. 2663 : this.unlock_frame();
  2664. 2664 : delete this.preview_id;
  2665. 2665 : }
  2666. 2666 : };
  2667. 2667 :
  2668. 2668 : this.get_frame_element = function(id)
  2669. 2669 : {
  2670. 2670 : var frame;
  2671. 2671 :
  2672. 2672 : if (id && (frame = document.getElementById(id)))
  2673. 2673 : return frame;
  2674. 2674 : };
  2675. 2675 :
  2676. 2676 : this.get_frame_window = function(id)
  2677. 2677 : {
  2678. 2678 : var frame = this.get_frame_element(id);
  2679. 2679 :
  2680. 2680 : if (frame && frame.name && window.frames)
  2681. 2681 : return window.frames[frame.name];
  2682. 2682 : };
  2683. 2683 :
  2684. 2684 : this.lock_frame = function(target)
  2685. 2685 : {
  2686. 2686 : var rc = this.is_framed() ? parent.rcmail : this;
  2687. 2687 :
  2688. 2688 : if (!rc.env.frame_lock)
  2689. 2689 : rc.env.frame_lock = rc.set_busy(true, 'loading');
  2690. 2690 :
  2691. 2691 : if (target.frameElement)
  2692. 2692 : $(target.frameElement).on('load.lock', function(e) {
  2693. 2693 : rc.unlock_frame();
  2694. 2694 : $(this).off('load.lock');
  2695. 2695 : });
  2696. 2696 : };
  2697. 2697 :
  2698. 2698 : this.unlock_frame = function()
  2699. 2699 : {
  2700. 2700 : if (this.env.frame_lock) {
  2701. 2701 : this.set_busy(false, null, this.env.frame_lock);
  2702. 2702 : this.env.frame_lock = null;
  2703. 2703 : }
  2704. 2704 : };
  2705. 2705 :
  2706. 2706 : // list a specific page
  2707. 2707 : this.list_page = function(page)
  2708. 2708 : {
  2709. 2709 : if (page == 'next')
  2710. 2710 : page = this.env.current_page+1;
  2711. 2711 : else if (page == 'last')
  2712. 2712 : page = this.env.pagecount;
  2713. 2713 : else if (page == 'prev' && this.env.current_page > 1)
  2714. 2714 : page = this.env.current_page-1;
  2715. 2715 : else if (page == 'first' && this.env.current_page > 1)
  2716. 2716 : page = 1;
  2717. 2717 :
  2718. 2718 : if (page > 0 && page <= this.env.pagecount) {
  2719. 2719 : this.env.current_page = page;
  2720. 2720 :
  2721. 2721 : if (this.task == 'addressbook' || this.contact_list)
  2722. 2722 : this.list_contacts(this.env.source, this.env.group, page);
  2723. 2723 : else if (this.task == 'mail')
  2724. 2724 : this.list_mailbox(this.env.mailbox, page);
  2725. 2725 : }
  2726. 2726 : };
  2727. 2727 :
  2728. 2728 : // sends request to check for recent messages
  2729. 2729 : this.checkmail = function()
  2730. 2730 : {
  2731. 2731 : var lock = this.set_busy(true, 'checkingmail'),
  2732. 2732 : params = this.check_recent_params();
  2733. 2733 :
  2734. 2734 : this.http_post('check-recent', params, lock);
  2735. 2735 : };
  2736. 2736 :
  2737. 2737 : // list messages of a specific mailbox using filter
  2738. 2738 : this.filter_mailbox = function(filter)
  2739. 2739 : {
  2740. 2740 : if (this.filter_disabled)
  2741. 2741 : return;
  2742. 2742 :
  2743. 2743 : var params = this.search_params(false, filter),
  2744. 2744 : lock = this.set_busy(true, 'searching');
  2745. 2745 :
  2746. 2746 : this.clear_message_list();
  2747. 2747 :
  2748. 2748 : // reset vars
  2749. 2749 : this.env.current_page = 1;
  2750. 2750 : this.env.search_filter = filter;
  2751. 2751 : this.http_request('search', params, lock);
  2752. 2752 : this.update_state({_mbox: params._mbox, _filter: filter, _scope: params._scope});
  2753. 2753 : };
  2754. 2754 :
  2755. 2755 : // reload the current message listing
  2756. 2756 : this.refresh_list = function()
  2757. 2757 : {
  2758. 2758 : this.list_mailbox(this.env.mailbox, this.env.current_page || 1, null, { _clear:1 }, true);
  2759. 2759 : if (this.message_list)
  2760. 2760 : this.message_list.clear_selection();
  2761. 2761 : };
  2762. 2762 :
  2763. 2763 : // list messages of a specific mailbox
  2764. 2764 : this.list_mailbox = function(mbox, page, sort, url, update_only)
  2765. 2765 : {
  2766. 2766 : var win, target = window;
  2767. 2767 :
  2768. 2768 : if (typeof url != 'object')
  2769. 2769 : url = {};
  2770. 2770 :
  2771. 2771 : if (!mbox)
  2772. 2772 : mbox = this.env.mailbox ? this.env.mailbox : 'INBOX';
  2773. 2773 :
  2774. 2774 : // add sort to url if set
  2775. 2775 : if (sort)
  2776. 2776 : url._sort = sort;
  2777. 2777 :
  2778. 2778 : // folder change, reset page, search scope, etc.
  2779. 2779 : if (this.env.mailbox != mbox) {
  2780. 2780 : page = 1;
  2781. 2781 : this.env.current_page = page;
  2782. 2782 : this.env.search_scope = 'base';
  2783. 2783 : this.select_all_mode = false;
  2784. 2784 : this.reset_search_filter();
  2785. 2785 : }
  2786. 2786 : // also send search request to get the right messages
  2787. 2787 : else if (this.env.search_request)
  2788. 2788 : url._search = this.env.search_request;
  2789. 2789 :
  2790. 2790 : if (!update_only) {
  2791. 2791 : // unselect selected messages and clear the list and message data
  2792. 2792 : this.clear_message_list();
  2793. 2793 :
  2794. 2794 : if (mbox != this.env.mailbox || (mbox == this.env.mailbox && !page && !sort))
  2795. 2795 : url._refresh = 1;
  2796. 2796 :
  2797. 2797 : this.select_folder(mbox, '', true);
  2798. 2798 : this.unmark_folder(mbox, 'recent', '', true);
  2799. 2799 : this.env.mailbox = mbox;
  2800. 2800 : }
  2801. 2801 :
  2802. 2802 : // load message list remotely
  2803. 2803 : if (this.gui_objects.messagelist) {
  2804. 2804 : this.list_mailbox_remote(mbox, page, url);
  2805. 2805 : return;
  2806. 2806 : }
  2807. 2807 :
  2808. 2808 : if (win = this.get_frame_window(this.env.contentframe)) {
  2809. 2809 : target = win;
  2810. 2810 : url._framed = 1;
  2811. 2811 : }
  2812. 2812 :
  2813. 2813 : if (this.env.uid)
  2814. 2814 : url._uid = this.env.uid;
  2815. 2815 :
  2816. 2816 : if (page)
  2817. 2817 : url._page = page;
  2818. 2818 :
  2819. 2819 : // load message list to target frame/window
  2820. 2820 : if (mbox) {
  2821. 2821 : url._mbox = mbox;
  2822. 2822 : this.set_busy(true, 'loading');
  2823. 2823 : this.location_href(url, target);
  2824. 2824 : }
  2825. 2825 : };
  2826. 2826 :
  2827. 2827 : this.clear_message_list = function()
  2828. 2828 : {
  2829. 2829 : this.env.messages = {};
  2830. 2830 :
  2831. 2831 : this.show_contentframe(false);
  2832. 2832 : if (this.message_list)
  2833. 2833 : this.message_list.clear(true);
  2834. 2834 : };
  2835. 2835 :
  2836. 2836 : // send remote request to load message list
  2837. 2837 : this.list_mailbox_remote = function(mbox, page, url)
  2838. 2838 : {
  2839. 2839 : var lock = this.set_busy(true, 'loading');
  2840. 2840 :
  2841. 2841 : if (typeof url != 'object')
  2842. 2842 : url = {};
  2843. 2843 :
  2844. 2844 : url._layout = this.env.layout
  2845. 2845 : url._mbox = mbox;
  2846. 2846 : url._page = page;
  2847. 2847 :
  2848. 2848 : this.http_request('list', url, lock);
  2849. 2849 : this.update_state({ _mbox: mbox, _page: (page && page > 1 ? page : null) });
  2850. 2850 : };
  2851. 2851 :
  2852. 2852 : // removes messages that doesn't exists from list selection array
  2853. 2853 : this.update_selection = function()
  2854. 2854 : {
  2855. 2855 : var list = this.message_list,
  2856. 2856 : selected = list.selection,
  2857. 2857 : rows = list.rows,
  2858. 2858 : i, selection = [];
  2859. 2859 :
  2860. 2860 : for (i in selected)
  2861. 2861 : if (rows[selected[i]])
  2862. 2862 : selection.push(selected[i]);
  2863. 2863 :
  2864. 2864 : list.selection = selection;
  2865. 2865 :
  2866. 2866 : // reset preview frame, if currently previewed message is not selected (has been removed)
  2867. 2867 : try {
  2868. 2868 : var win = this.get_frame_window(this.env.contentframe),
  2869. 2869 : id = win.rcmail.env.uid;
  2870. 2870 :
  2871. 2871 : if (id && !list.in_selection(id))
  2872. 2872 : this.show_contentframe(false);
  2873. 2873 : }
  2874. 2874 : catch (e) {};
  2875. 2875 : };
  2876. 2876 :
  2877. 2877 : // expand all threads with unread children
  2878. 2878 : this.expand_unread = function()
  2879. 2879 : {
  2880. 2880 : var r, tbody = this.message_list.tbody,
  2881. 2881 : new_row = tbody.firstChild;
  2882. 2882 :
  2883. 2883 : while (new_row) {
  2884. 2884 : if (new_row.nodeType == 1 && (r = this.message_list.rows[new_row.uid]) && r.unread_children) {
  2885. 2885 : this.message_list.expand_all(r);
  2886. 2886 : this.set_unread_children(r.uid);
  2887. 2887 : }
  2888. 2888 :
  2889. 2889 : new_row = new_row.nextSibling;
  2890. 2890 : }
  2891. 2891 :
  2892. 2892 : return false;
  2893. 2893 : };
  2894. 2894 :
  2895. 2895 : // thread expanding/collapsing handler
  2896. 2896 : this.expand_message_row = function(e, uid)
  2897. 2897 : {
  2898. 2898 : var row = this.message_list.rows[uid];
  2899. 2899 :
  2900. 2900 : // handle unread_children/flagged_children mark
  2901. 2901 : row.expanded = !row.expanded;
  2902. 2902 : this.set_unread_children(uid);
  2903. 2903 : this.set_flagged_children(uid);
  2904. 2904 : row.expanded = !row.expanded;
  2905. 2905 :
  2906. 2906 : this.message_list.expand_row(e, uid);
  2907. 2907 : };
  2908. 2908 :
  2909. 2909 : // message list expanding
  2910. 2910 : this.expand_threads = function()
  2911. 2911 : {
  2912. 2912 : if (!this.env.threading || !this.env.autoexpand_threads || !this.message_list)
  2913. 2913 : return;
  2914. 2914 :
  2915. 2915 : switch (this.env.autoexpand_threads) {
  2916. 2916 : case 2: this.expand_unread(); break;
  2917. 2917 : case 1: this.message_list.expand_all(); break;
  2918. 2918 : }
  2919. 2919 : };
  2920. 2920 :
  2921. 2921 : // Initializes threads indicators/expanders after list update
  2922. 2922 : this.init_threads = function(roots, mbox)
  2923. 2923 : {
  2924. 2924 : // #1487752
  2925. 2925 : if (mbox && mbox != this.env.mailbox)
  2926. 2926 : return false;
  2927. 2927 :
  2928. 2928 : for (var n=0, len=roots.length; n<len; n++)
  2929. 2929 : this.add_tree_icons(roots[n]);
  2930. 2930 : this.expand_threads();
  2931. 2931 : };
  2932. 2932 :
  2933. 2933 : // adds threads tree icons to the list (or specified thread)
  2934. 2934 : this.add_tree_icons = function(root)
  2935. 2935 : {
  2936. 2936 : var i, l, r, n, len, pos, tmp = [], uid = [],
  2937. 2937 : row, rows = this.message_list.rows;
  2938. 2938 :
  2939. 2939 : if (root)
  2940. 2940 : row = rows[root] ? rows[root].obj : null;
  2941. 2941 : else
  2942. 2942 : row = this.message_list.tbody.firstChild;
  2943. 2943 :
  2944. 2944 : while (row) {
  2945. 2945 : if (row.nodeType == 1 && (r = rows[row.uid])) {
  2946. 2946 : if (r.depth) {
  2947. 2947 : for (i=tmp.length-1; i>=0; i--) {
  2948. 2948 : len = tmp[i].length;
  2949. 2949 : if (len > r.depth) {
  2950. 2950 : pos = len - r.depth;
  2951. 2951 : if (!(tmp[i][pos] & 2))
  2952. 2952 : tmp[i][pos] = tmp[i][pos] ? tmp[i][pos]+2 : 2;
  2953. 2953 : }
  2954. 2954 : else if (len == r.depth) {
  2955. 2955 : if (!(tmp[i][0] & 2))
  2956. 2956 : tmp[i][0] += 2;
  2957. 2957 : }
  2958. 2958 : if (r.depth > len)
  2959. 2959 : break;
  2960. 2960 : }
  2961. 2961 :
  2962. 2962 : tmp.push(new Array(r.depth));
  2963. 2963 : tmp[tmp.length-1][0] = 1;
  2964. 2964 : uid.push(r.uid);
  2965. 2965 : }
  2966. 2966 : else {
  2967. 2967 : if (tmp.length) {
  2968. 2968 : for (i in tmp) {
  2969. 2969 : this.set_tree_icons(uid[i], tmp[i]);
  2970. 2970 : }
  2971. 2971 : tmp = [];
  2972. 2972 : uid = [];
  2973. 2973 : }
  2974. 2974 : if (root && row != rows[root].obj)
  2975. 2975 : break;
  2976. 2976 : }
  2977. 2977 : }
  2978. 2978 : row = row.nextSibling;
  2979. 2979 : }
  2980. 2980 :
  2981. 2981 : if (tmp.length) {
  2982. 2982 : for (i in tmp) {
  2983. 2983 : this.set_tree_icons(uid[i], tmp[i]);
  2984. 2984 : }
  2985. 2985 : }
  2986. 2986 : };
  2987. 2987 :
  2988. 2988 : // adds tree icons to specified message row
  2989. 2989 : this.set_tree_icons = function(uid, tree)
  2990. 2990 : {
  2991. 2991 : var i, divs = [], html = '', len = tree.length;
  2992. 2992 :
  2993. 2993 : for (i=0; i<len; i++) {
  2994. 2994 : if (tree[i] > 2)
  2995. 2995 : divs.push({'class': 'l3', width: 15});
  2996. 2996 : else if (tree[i] > 1)
  2997. 2997 : divs.push({'class': 'l2', width: 15});
  2998. 2998 : else if (tree[i] > 0)
  2999. 2999 : divs.push({'class': 'l1', width: 15});
  3000. 3000 : // separator div
  3001. 3001 : else if (divs.length && !divs[divs.length-1]['class'])
  3002. 3002 : divs[divs.length-1].width += 15;
  3003. 3003 : else
  3004. 3004 : divs.push({'class': null, width: 15});
  3005. 3005 : }
  3006. 3006 :
  3007. 3007 : for (i=divs.length-1; i>=0; i--) {
  3008. 3008 : if (divs[i]['class'])
  3009. 3009 : html += '<div class="tree '+divs[i]['class']+'" />';
  3010. 3010 : else
  3011. 3011 : html += '<div style="width:'+divs[i].width+'px" />';
  3012. 3012 : }
  3013. 3013 :
  3014. 3014 : if (html)
  3015. 3015 : $('#rcmtab'+this.html_identifier(uid, true)).html(html);
  3016. 3016 : };
  3017. 3017 :
  3018. 3018 : // update parent in a thread
  3019. 3019 : this.update_thread_root = function(uid, flag)
  3020. 3020 : {
  3021. 3021 : if (!this.env.threading)
  3022. 3022 : return;
  3023. 3023 :
  3024. 3024 : var root = this.message_list.find_root(uid);
  3025. 3025 :
  3026. 3026 : if (uid == root)
  3027. 3027 : return;
  3028. 3028 :
  3029. 3029 : var p = this.message_list.rows[root];
  3030. 3030 :
  3031. 3031 : if (flag == 'read' && p.unread_children) {
  3032. 3032 : p.unread_children--;
  3033. 3033 : }
  3034. 3034 : else if (flag == 'unread' && p.has_children) {
  3035. 3035 : // unread_children may be undefined
  3036. 3036 : p.unread_children = (p.unread_children || 0) + 1;
  3037. 3037 : }
  3038. 3038 : else if (flag == 'unflagged' && p.flagged_children) {
  3039. 3039 : p.flagged_children--;
  3040. 3040 : }
  3041. 3041 : else if (flag == 'flagged' && p.has_children) {
  3042. 3042 : p.flagged_children = (p.flagged_children || 0) + 1;
  3043. 3043 : }
  3044. 3044 : else {
  3045. 3045 : return;
  3046. 3046 : }
  3047. 3047 :
  3048. 3048 : this.set_message_icon(root);
  3049. 3049 : this.set_unread_children(root);
  3050. 3050 : this.set_flagged_children(root);
  3051. 3051 : };
  3052. 3052 :
  3053. 3053 : // update thread indicators for all messages in a thread below the specified message
  3054. 3054 : // return number of removed/added root level messages
  3055. 3055 : this.update_thread = function(uid)
  3056. 3056 : {
  3057. 3057 : if (!this.env.threading || !this.message_list.rows[uid])
  3058. 3058 : return 0;
  3059. 3059 :
  3060. 3060 : var r, parent, count = 0,
  3061. 3061 : list = this.message_list,
  3062. 3062 : rows = list.rows,
  3063. 3063 : row = rows[uid],
  3064. 3064 : depth = rows[uid].depth,
  3065. 3065 : roots = [];
  3066. 3066 :
  3067. 3067 : if (!row.depth) // root message: decrease roots count
  3068. 3068 : count--;
  3069. 3069 :
  3070. 3070 : // update unread_children for thread root
  3071. 3071 : if (row.depth && row.unread) {
  3072. 3072 : parent = list.find_root(uid);
  3073. 3073 : rows[parent].unread_children--;
  3074. 3074 : this.set_unread_children(parent);
  3075. 3075 : }
  3076. 3076 :
  3077. 3077 : // update unread_children for thread root
  3078. 3078 : if (row.depth && row.flagged) {
  3079. 3079 : parent = list.find_root(uid);
  3080. 3080 : rows[parent].flagged_children--;
  3081. 3081 : this.set_flagged_children(parent);
  3082. 3082 : }
  3083. 3083 :
  3084. 3084 : parent = row.parent_uid;
  3085. 3085 :
  3086. 3086 : // children
  3087. 3087 : row = row.obj.nextSibling;
  3088. 3088 : while (row) {
  3089. 3089 : if (row.nodeType == 1 && (r = rows[row.uid])) {
  3090. 3090 : if (!r.depth || r.depth <= depth)
  3091. 3091 : break;
  3092. 3092 :
  3093. 3093 : r.depth--; // move left
  3094. 3094 : // reset width and clear the content of a tab, icons will be added later
  3095. 3095 : $('#rcmtab'+r.id).width(r.depth * 15).html('');
  3096. 3096 : if (!r.depth) { // a new root
  3097. 3097 : count++; // increase roots count
  3098. 3098 : r.parent_uid = 0;
  3099. 3099 : if (r.has_children) {
  3100. 3100 : // replace 'leaf' with 'collapsed'
  3101. 3101 : $('#' + r.id + ' .leaf').first()
  3102. 3102 : .attr('id', 'rcmexpando' + r.id)
  3103. 3103 : .attr('class', (r.obj.style.display != 'none' ? 'expanded' : 'collapsed'))
  3104. 3104 : .mousedown({uid: r.uid}, function(e) {
  3105. 3105 : return ref.expand_message_row(e, e.data.uid);
  3106. 3106 : });
  3107. 3107 :
  3108. 3108 : r.unread_children = 0;
  3109. 3109 : roots.push(r);
  3110. 3110 : }
  3111. 3111 : // show if it was hidden
  3112. 3112 : if (r.obj.style.display == 'none')
  3113. 3113 : $(r.obj).show();
  3114. 3114 : }
  3115. 3115 : else {
  3116. 3116 : if (r.depth == depth)
  3117. 3117 : r.parent_uid = parent;
  3118. 3118 : if (r.unread && roots.length)
  3119. 3119 : roots[roots.length-1].unread_children++;
  3120. 3120 : }
  3121. 3121 : }
  3122. 3122 : row = row.nextSibling;
  3123. 3123 : }
  3124. 3124 :
  3125. 3125 : // update unread_children/flagged_children for roots
  3126. 3126 : for (r=0; r<roots.length; r++) {
  3127. 3127 : this.set_unread_children(roots[r].uid);
  3128. 3128 : this.set_flagged_children(roots[r].uid);
  3129. 3129 : }
  3130. 3130 :
  3131. 3131 : return count;
  3132. 3132 : };
  3133. 3133 :
  3134. 3134 : this.delete_excessive_thread_rows = function()
  3135. 3135 : {
  3136. 3136 : var rows = this.message_list.rows,
  3137. 3137 : tbody = this.message_list.tbody,
  3138. 3138 : row = tbody.firstChild,
  3139. 3139 : cnt = this.env.pagesize + 1;
  3140. 3140 :
  3141. 3141 : while (row) {
  3142. 3142 : if (row.nodeType == 1 && (r = rows[row.uid])) {
  3143. 3143 : if (!r.depth && cnt)
  3144. 3144 : cnt--;
  3145. 3145 :
  3146. 3146 : if (!cnt)
  3147. 3147 : this.message_list.remove_row(row.uid);
  3148. 3148 : }
  3149. 3149 : row = row.nextSibling;
  3150. 3150 : }
  3151. 3151 : };
  3152. 3152 :
  3153. 3153 : // set message icon
  3154. 3154 : this.set_message_icon = function(uid)
  3155. 3155 : {
  3156. 3156 : var css_class, label = '',
  3157. 3157 : row = this.message_list.rows[uid];
  3158. 3158 :
  3159. 3159 : if (!row)
  3160. 3160 : return false;
  3161. 3161 :
  3162. 3162 : if (row.icon) {
  3163. 3163 : css_class = 'msgicon';
  3164. 3164 : if (row.deleted) {
  3165. 3165 : css_class += ' deleted';
  3166. 3166 : label += this.get_label('deleted') + ' ';
  3167. 3167 : }
  3168. 3168 : else if (row.unread) {
  3169. 3169 : css_class += ' unread';
  3170. 3170 : label += this.get_label('unread') + ' ';
  3171. 3171 : }
  3172. 3172 : else if (row.unread_children)
  3173. 3173 : css_class += ' unreadchildren';
  3174. 3174 : if (row.msgicon == row.icon) {
  3175. 3175 : if (row.replied) {
  3176. 3176 : css_class += ' replied';
  3177. 3177 : label += this.get_label('replied') + ' ';
  3178. 3178 : }
  3179. 3179 : if (row.forwarded) {
  3180. 3180 : css_class += ' forwarded';
  3181. 3181 : label += this.get_label('forwarded') + ' ';
  3182. 3182 : }
  3183. 3183 : css_class += ' status';
  3184. 3184 : }
  3185. 3185 :
  3186. 3186 : $(row.icon).attr({'class': css_class, title: label});
  3187. 3187 : }
  3188. 3188 :
  3189. 3189 : if (row.msgicon && row.msgicon != row.icon) {
  3190. 3190 : label = '';
  3191. 3191 : css_class = 'msgicon';
  3192. 3192 : if (!row.unread && row.unread_children) {
  3193. 3193 : css_class += ' unreadchildren';
  3194. 3194 : }
  3195. 3195 : if (row.replied) {
  3196. 3196 : css_class += ' replied';
  3197. 3197 : label += this.get_label('replied') + ' ';
  3198. 3198 : }
  3199. 3199 : if (row.forwarded) {
  3200. 3200 : css_class += ' forwarded';
  3201. 3201 : label += this.get_label('forwarded') + ' ';
  3202. 3202 : }
  3203. 3203 :
  3204. 3204 : $(row.msgicon).attr({'class': css_class, title: label});
  3205. 3205 : }
  3206. 3206 :
  3207. 3207 : if (row.flagicon) {
  3208. 3208 : css_class = (row.flagged ? 'flagged' : 'unflagged');
  3209. 3209 : label = this.get_label(css_class);
  3210. 3210 : $(row.flagicon).attr('class', css_class)
  3211. 3211 : .attr({'aria-label': label, title: label});
  3212. 3212 : }
  3213. 3213 : };
  3214. 3214 :
  3215. 3215 : // set message status
  3216. 3216 : this.set_message_status = function(uid, flag, status)
  3217. 3217 : {
  3218. 3218 : var row = this.message_list.rows[uid];
  3219. 3219 :
  3220. 3220 : if (!row)
  3221. 3221 : return false;
  3222. 3222 :
  3223. 3223 : if (flag == 'unread') {
  3224. 3224 : if (row.unread != status)
  3225. 3225 : this.update_thread_root(uid, status ? 'unread' : 'read');
  3226. 3226 : }
  3227. 3227 : else if (flag == 'flagged') {
  3228. 3228 : this.update_thread_root(uid, status ? 'flagged' : 'unflagged');
  3229. 3229 : }
  3230. 3230 :
  3231. 3231 : if ($.inArray(flag, ['unread', 'deleted', 'replied', 'forwarded', 'flagged']) > -1)
  3232. 3232 : row[flag] = status;
  3233. 3233 : };
  3234. 3234 :
  3235. 3235 : // set message row status, class and icon
  3236. 3236 : this.set_message = function(uid, flag, status)
  3237. 3237 : {
  3238. 3238 : var row = this.message_list && this.message_list.rows[uid];
  3239. 3239 :
  3240. 3240 : if (!row)
  3241. 3241 : return false;
  3242. 3242 :
  3243. 3243 : if (flag)
  3244. 3244 : this.set_message_status(uid, flag, status);
  3245. 3245 :
  3246. 3246 : if ($.inArray(flag, ['unread', 'deleted', 'flagged']) > -1)
  3247. 3247 : $(row.obj)[row[flag] ? 'addClass' : 'removeClass'](flag);
  3248. 3248 :
  3249. 3249 : this.set_unread_children(uid);
  3250. 3250 : this.set_message_icon(uid);
  3251. 3251 : };
  3252. 3252 :
  3253. 3253 : // sets unroot (unread_children) class of parent row
  3254. 3254 : this.set_unread_children = function(uid)
  3255. 3255 : {
  3256. 3256 : var row = this.message_list.rows[uid];
  3257. 3257 :
  3258. 3258 : if (row.parent_uid)
  3259. 3259 : return;
  3260. 3260 :
  3261. 3261 : var enable = !row.unread && row.unread_children && !row.expanded;
  3262. 3262 : $(row.obj)[enable ? 'addClass' : 'removeClass']('unroot');
  3263. 3263 : };
  3264. 3264 :
  3265. 3265 : // sets flaggedroot (flagged_children) class of parent row
  3266. 3266 : this.set_flagged_children = function(uid)
  3267. 3267 : {
  3268. 3268 : var row = this.message_list.rows[uid];
  3269. 3269 :
  3270. 3270 : if (row.parent_uid)
  3271. 3271 : return;
  3272. 3272 :
  3273. 3273 : var enable = row.flagged_children && !row.expanded;
  3274. 3274 : $(row.obj)[enable ? 'addClass' : 'removeClass']('flaggedroot');
  3275. 3275 : };
  3276. 3276 :
  3277. 3277 : // copy selected messages to the specified mailbox
  3278. 3278 : this.copy_messages = function(mbox, event, uids)
  3279. 3279 : {
  3280. 3280 : if (mbox && typeof mbox === 'object') {
  3281. 3281 : if (mbox.uids) uids = mbox.uids;
  3282. 3282 : mbox = mbox.id;
  3283. 3283 : }
  3284. 3284 : else if (!mbox) {
  3285. 3285 : uids = this.env.uid ? [this.env.uid] : this.message_list.get_selection();
  3286. 3286 : return this.folder_selector(event, function(folder, obj) {
  3287. 3287 : ref.command('copy', {id: folder, uids: uids}, obj, event, true);
  3288. 3288 : });
  3289. 3289 : }
  3290. 3290 :
  3291. 3291 : // exit if current or no mailbox specified
  3292. 3292 : if (!mbox || mbox == this.env.mailbox)
  3293. 3293 : return;
  3294. 3294 :
  3295. 3295 : var post_data = this.selection_post_data({_target_mbox: mbox, _uid: uids});
  3296. 3296 :
  3297. 3297 : // exit if selection is empty
  3298. 3298 : if (!post_data._uid)
  3299. 3299 : return;
  3300. 3300 :
  3301. 3301 : // send request to server
  3302. 3302 : this.http_post('copy', post_data, this.display_message('copyingmessage', 'loading'));
  3303. 3303 : };
  3304. 3304 :
  3305. 3305 : // move selected messages to the specified mailbox
  3306. 3306 : this.move_messages = function(mbox, event, uids)
  3307. 3307 : {
  3308. 3308 : if (mbox && typeof mbox === 'object') {
  3309. 3309 : if (mbox.uids) uids = mbox.uids;
  3310. 3310 : mbox = mbox.id;
  3311. 3311 : }
  3312. 3312 : else if (!mbox) {
  3313. 3313 : uids = this.env.uid ? [this.env.uid] : this.message_list.get_selection();
  3314. 3314 : return this.folder_selector(event, function(folder, obj) {
  3315. 3315 : ref.command('move', {id: folder, uids: uids}, obj, event, true);
  3316. 3316 : });
  3317. 3317 : }
  3318. 3318 :
  3319. 3319 : // exit if current or no mailbox specified
  3320. 3320 : if (!mbox || (mbox == this.env.mailbox && !this.is_multifolder_listing()))
  3321. 3321 : return;
  3322. 3322 :
  3323. 3323 : var lock = false, post_data = this.selection_post_data({_target_mbox: mbox, _uid: uids});
  3324. 3324 :
  3325. 3325 : // exit if selection is empty
  3326. 3326 : if (!post_data._uid)
  3327. 3327 : return;
  3328. 3328 :
  3329. 3329 : // show wait message
  3330. 3330 : if (this.env.action == 'show')
  3331. 3331 : lock = this.set_busy(true, 'movingmessage');
  3332. 3332 :
  3333. 3333 : // Hide message command buttons until a message is selected
  3334. 3334 : this.enable_command(this.env.message_commands, false);
  3335. 3335 :
  3336. 3336 : this.with_selected_messages('move', post_data, lock);
  3337. 3337 :
  3338. 3338 : if (this.env.action != 'show')
  3339. 3339 : this.show_contentframe(false);
  3340. 3340 : };
  3341. 3341 :
  3342. 3342 : // delete selected messages from the current mailbox
  3343. 3343 : this.delete_messages = function(event)
  3344. 3344 : {
  3345. 3345 : var list = this.message_list, trash = this.env.trash_mailbox;
  3346. 3346 :
  3347. 3347 : // if config is set to flag for deletion
  3348. 3348 : if (this.env.flag_for_deletion) {
  3349. 3349 : this.mark_message('delete');
  3350. 3350 : return false;
  3351. 3351 : }
  3352. 3352 : // if there isn't a defined trash mailbox or we are in it
  3353. 3353 : else if (!trash || this.env.mailbox == trash)
  3354. 3354 : this.permanently_remove_messages();
  3355. 3355 : // we're in Junk folder and delete_junk is enabled
  3356. 3356 : else if (this.env.delete_junk && this.env.junk_mailbox && this.env.mailbox == this.env.junk_mailbox)
  3357. 3357 : this.permanently_remove_messages();
  3358. 3358 : // if there is a trash mailbox defined and we're not currently in it
  3359. 3359 : else {
  3360. 3360 : // if shift was pressed delete it immediately
  3361. 3361 : if ((list && list.modkey == SHIFT_KEY) || (event && rcube_event.get_modifier(event) == SHIFT_KEY)) {
  3362. 3362 : this.confirm_dialog(this.get_label('deletemessagesconfirm'), 'delete', function() {
  3363. 3363 : ref.permanently_remove_messages();
  3364. 3364 : });
  3365. 3365 : }
  3366. 3366 : else
  3367. 3367 : this.move_messages(trash);
  3368. 3368 : }
  3369. 3369 :
  3370. 3370 : return true;
  3371. 3371 : };
  3372. 3372 :
  3373. 3373 : // delete the selected messages permanently
  3374. 3374 : this.permanently_remove_messages = function()
  3375. 3375 : {
  3376. 3376 : var post_data = this.selection_post_data();
  3377. 3377 :
  3378. 3378 : // exit if selection is empty
  3379. 3379 : if (!post_data._uid)
  3380. 3380 : return;
  3381. 3381 :
  3382. 3382 : this.with_selected_messages('delete', post_data);
  3383. 3383 : this.show_contentframe(false);
  3384. 3384 : };
  3385. 3385 :
  3386. 3386 : // Send a specific move/delete request with UIDs of all selected messages
  3387. 3387 : this.with_selected_messages = function(action, post_data, lock, http_action)
  3388. 3388 : {
  3389. 3389 : var count = 0, msg,
  3390. 3390 : remove = (action == 'delete' || !this.is_multifolder_listing());
  3391. 3391 :
  3392. 3392 : // update the list (remove rows, clear selection)
  3393. 3393 : if (this.message_list) {
  3394. 3394 : var n, len, id, root, roots = [],
  3395. 3395 : selection = post_data._uid,
  3396. 3396 : display_next = this.check_display_next();
  3397. 3397 :
  3398. 3398 : if (selection === '*')
  3399. 3399 : selection = this.message_list.get_selection();
  3400. 3400 : else if (typeof selection == 'string')
  3401. 3401 : selection = selection.split(',');
  3402. 3402 :
  3403. 3403 : for (n=0, len=selection.length; n<len; n++) {
  3404. 3404 : id = selection[n];
  3405. 3405 :
  3406. 3406 : if (this.env.threading) {
  3407. 3407 : count += this.update_thread(id);
  3408. 3408 : root = this.message_list.find_root(id);
  3409. 3409 : if (root != id && $.inArray(root, roots) < 0) {
  3410. 3410 : roots.push(root);
  3411. 3411 : }
  3412. 3412 : }
  3413. 3413 :
  3414. 3414 : if (remove)
  3415. 3415 : this.message_list.remove_row(id, display_next && n == selection.length-1);
  3416. 3416 : }
  3417. 3417 :
  3418. 3418 : // make sure there are no selected rows
  3419. 3419 : if (!display_next && remove)
  3420. 3420 : this.message_list.clear_selection();
  3421. 3421 :
  3422. 3422 : // update thread tree icons
  3423. 3423 : for (n=0, len=roots.length; n<len; n++) {
  3424. 3424 : this.add_tree_icons(roots[n]);
  3425. 3425 : }
  3426. 3426 : }
  3427. 3427 :
  3428. 3428 : if (count < 0)
  3429. 3429 : post_data._count = (count*-1);
  3430. 3430 : // remove threads from the end of the list
  3431. 3431 : else if (count > 0 && remove)
  3432. 3432 : this.delete_excessive_thread_rows();
  3433. 3433 :
  3434. 3434 : if (!remove)
  3435. 3435 : post_data._refresh = 1;
  3436. 3436 :
  3437. 3437 : if (!lock) {
  3438. 3438 : msg = action == 'move' ? 'movingmessage' : 'deletingmessage';
  3439. 3439 : lock = this.display_message(msg, 'loading');
  3440. 3440 : }
  3441. 3441 :
  3442. 3442 : // send request to server
  3443. 3443 : this.http_post(http_action || action, post_data, lock);
  3444. 3444 : };
  3445. 3445 :
  3446. 3446 : // build post data for message delete/move/copy/flag requests
  3447. 3447 : this.selection_post_data = function(data)
  3448. 3448 : {
  3449. 3449 : if (typeof(data) != 'object')
  3450. 3450 : data = {};
  3451. 3451 :
  3452. 3452 : if (!data._uid)
  3453. 3453 : data._uid = this.env.uid ? [this.env.uid] : this.message_list.get_selection();
  3454. 3454 :
  3455. 3455 : data._mbox = this.env.mailbox;
  3456. 3456 : data._uid = this.uids_to_list(data._uid);
  3457. 3457 :
  3458. 3458 : if (this.env.action)
  3459. 3459 : data._from = this.env.action;
  3460. 3460 :
  3461. 3461 : // also send search request to get the right messages
  3462. 3462 : if (this.env.search_request)
  3463. 3463 : data._search = this.env.search_request;
  3464. 3464 :
  3465. 3465 : if (this.env.display_next && this.env.next_uid)
  3466. 3466 : data._next_uid = this.env.next_uid;
  3467. 3467 :
  3468. 3468 : return data;
  3469. 3469 : };
  3470. 3470 :
  3471. 3471 : this.check_display_next = function()
  3472. 3472 : {
  3473. 3473 : return this.env.display_next && (this.preview_id || !this.env.contentframe);
  3474. 3474 : };
  3475. 3475 :
  3476. 3476 : // set a specific flag to one or more messages
  3477. 3477 : this.mark_message = function(flag, uid)
  3478. 3478 : {
  3479. 3479 : var a_uids = [], r_uids = [], len, n, id,
  3480. 3480 : list = this.message_list;
  3481. 3481 :
  3482. 3482 : if (uid)
  3483. 3483 : a_uids[0] = uid;
  3484. 3484 : else if (this.env.uid)
  3485. 3485 : a_uids[0] = this.env.uid;
  3486. 3486 : else if (list)
  3487. 3487 : a_uids = list.get_selection();
  3488. 3488 :
  3489. 3489 : if (!list)
  3490. 3490 : r_uids = a_uids;
  3491. 3491 : else {
  3492. 3492 : list.focus();
  3493. 3493 : for (n=0, len=a_uids.length; n<len; n++) {
  3494. 3494 : id = a_uids[n];
  3495. 3495 : if ((flag == 'read' && list.rows[id].unread)
  3496. 3496 : || (flag == 'unread' && !list.rows[id].unread)
  3497. 3497 : || (flag == 'delete' && !list.rows[id].deleted)
  3498. 3498 : || (flag == 'undelete' && list.rows[id].deleted)
  3499. 3499 : || (flag == 'flagged' && !list.rows[id].flagged)
  3500. 3500 : || (flag == 'unflagged' && list.rows[id].flagged))
  3501. 3501 : {
  3502. 3502 : r_uids.push(id);
  3503. 3503 : }
  3504. 3504 : }
  3505. 3505 : }
  3506. 3506 :
  3507. 3507 : // nothing to do
  3508. 3508 : if (!r_uids.length && !this.select_all_mode)
  3509. 3509 : return;
  3510. 3510 :
  3511. 3511 : switch (flag) {
  3512. 3512 : case 'read':
  3513. 3513 : case 'unread':
  3514. 3514 : this.toggle_read_status(flag, r_uids);
  3515. 3515 : break;
  3516. 3516 : case 'delete':
  3517. 3517 : case 'undelete':
  3518. 3518 : this.toggle_delete_status(r_uids);
  3519. 3519 : break;
  3520. 3520 : case 'flagged':
  3521. 3521 : case 'unflagged':
  3522. 3522 : this.toggle_flagged_status(flag, a_uids);
  3523. 3523 : break;
  3524. 3524 : }
  3525. 3525 : };
  3526. 3526 :
  3527. 3527 : // set class to read/unread
  3528. 3528 : this.toggle_read_status = function(flag, a_uids)
  3529. 3529 : {
  3530. 3530 : var i, len = a_uids.length,
  3531. 3531 : post_data = this.selection_post_data({_uid: a_uids, _flag: flag}),
  3532. 3532 : lock = this.display_message('markingmessage', 'loading');
  3533. 3533 :
  3534. 3534 : // mark all message rows as read/unread
  3535. 3535 : for (i=0; i<len; i++)
  3536. 3536 : this.set_message(a_uids[i], 'unread', (flag == 'unread' ? true : false));
  3537. 3537 :
  3538. 3538 : this.http_post('mark', post_data, lock);
  3539. 3539 : };
  3540. 3540 :
  3541. 3541 : // set image to flagged or unflagged
  3542. 3542 : this.toggle_flagged_status = function(flag, a_uids)
  3543. 3543 : {
  3544. 3544 : var i, len = a_uids.length,
  3545. 3545 : win = this.env.contentframe ? this.get_frame_window(this.env.contentframe) : window,
  3546. 3546 : post_data = this.selection_post_data({_uid: a_uids, _flag: flag}),
  3547. 3547 : lock = this.display_message('markingmessage', 'loading');
  3548. 3548 :
  3549. 3549 : // mark all message rows as flagged/unflagged
  3550. 3550 : for (i=0; i<len; i++)
  3551. 3551 : this.set_message(a_uids[i], 'flagged', (flag == 'flagged' ? true : false));
  3552. 3552 :
  3553. 3553 : if (this.env.action == 'show' || $.inArray(this.preview_id, a_uids) >= 0)
  3554. 3554 : $(win.document.body)[flag == 'flagged' ? 'addClass' : 'removeClass']('status-flagged');
  3555. 3555 :
  3556. 3556 : this.http_post('mark', post_data, lock);
  3557. 3557 : };
  3558. 3558 :
  3559. 3559 : // mark all message rows as deleted/undeleted
  3560. 3560 : this.toggle_delete_status = function(a_uids)
  3561. 3561 : {
  3562. 3562 : var i, uid, all_deleted = true,
  3563. 3563 : len = a_uids.length,
  3564. 3564 : rows = this.message_list ? this.message_list.rows : {};
  3565. 3565 :
  3566. 3566 : if (len == 1) {
  3567. 3567 : if (!this.message_list || (rows[a_uids[0]] && !rows[a_uids[0]].deleted))
  3568. 3568 : this.flag_as_deleted(a_uids);
  3569. 3569 : else
  3570. 3570 : this.flag_as_undeleted(a_uids);
  3571. 3571 :
  3572. 3572 : return true;
  3573. 3573 : }
  3574. 3574 :
  3575. 3575 : for (i=0; i<len; i++) {
  3576. 3576 : uid = a_uids[i];
  3577. 3577 : if (rows[uid] && !rows[uid].deleted) {
  3578. 3578 : all_deleted = false;
  3579. 3579 : break;
  3580. 3580 : }
  3581. 3581 : }
  3582. 3582 :
  3583. 3583 : if (all_deleted)
  3584. 3584 : this.flag_as_undeleted(a_uids);
  3585. 3585 : else
  3586. 3586 : this.flag_as_deleted(a_uids);
  3587. 3587 :
  3588. 3588 : return true;
  3589. 3589 : };
  3590. 3590 :
  3591. 3591 : this.flag_as_undeleted = function(a_uids)
  3592. 3592 : {
  3593. 3593 : var i, len = a_uids.length,
  3594. 3594 : post_data = this.selection_post_data({_uid: a_uids, _flag: 'undelete'}),
  3595. 3595 : lock = this.display_message('markingmessage', 'loading');
  3596. 3596 :
  3597. 3597 : for (i=0; i<len; i++)
  3598. 3598 : this.set_message(a_uids[i], 'deleted', false);
  3599. 3599 :
  3600. 3600 : this.http_post('mark', post_data, lock);
  3601. 3601 : };
  3602. 3602 :
  3603. 3603 : this.flag_as_deleted = function(a_uids)
  3604. 3604 : {
  3605. 3605 : var r_uids = [],
  3606. 3606 : post_data = this.selection_post_data({_uid: a_uids, _flag: 'delete'}),
  3607. 3607 : lock = this.display_message('markingmessage', 'loading'),
  3608. 3608 : list = this.message_list,
  3609. 3609 : rows = list ? list.rows : {},
  3610. 3610 : count = 0,
  3611. 3611 : display_next = this.check_display_next();
  3612. 3612 :
  3613. 3613 : for (var i=0, len=a_uids.length; i<len; i++) {
  3614. 3614 : uid = a_uids[i];
  3615. 3615 : if (rows[uid]) {
  3616. 3616 : if (rows[uid].unread)
  3617. 3617 : r_uids[r_uids.length] = uid;
  3618. 3618 :
  3619. 3619 : if (this.env.skip_deleted) {
  3620. 3620 : count += this.update_thread(uid);
  3621. 3621 : list.remove_row(uid, display_next && i == list.get_selection(false).length-1);
  3622. 3622 : }
  3623. 3623 : else
  3624. 3624 : this.set_message(uid, 'deleted', true);
  3625. 3625 : }
  3626. 3626 : }
  3627. 3627 :
  3628. 3628 : // make sure there are no selected rows
  3629. 3629 : if (this.env.skip_deleted && list) {
  3630. 3630 : if (!display_next || !list.rowcount)
  3631. 3631 : list.clear_selection();
  3632. 3632 : if (count < 0)
  3633. 3633 : post_data._count = (count*-1);
  3634. 3634 : else if (count > 0)
  3635. 3635 : // remove threads from the end of the list
  3636. 3636 : this.delete_excessive_thread_rows();
  3637. 3637 : }
  3638. 3638 :
  3639. 3639 : // set of messages to mark as seen
  3640. 3640 : if (r_uids.length)
  3641. 3641 : post_data._ruid = this.uids_to_list(r_uids);
  3642. 3642 :
  3643. 3643 : if (this.env.skip_deleted && this.env.display_next && this.env.next_uid)
  3644. 3644 : post_data._next_uid = this.env.next_uid;
  3645. 3645 :
  3646. 3646 : this.http_post('mark', post_data, lock);
  3647. 3647 : };
  3648. 3648 :
  3649. 3649 : // flag as read without mark request (called from backend)
  3650. 3650 : // argument should be a coma-separated list of uids
  3651. 3651 : this.flag_deleted_as_read = function(uids)
  3652. 3652 : {
  3653. 3653 : var uid, i, len,
  3654. 3654 : rows = this.message_list ? this.message_list.rows : {};
  3655. 3655 :
  3656. 3656 : if (typeof uids == 'string')
  3657. 3657 : uids = uids.split(',');
  3658. 3658 :
  3659. 3659 : for (i=0, len=uids.length; i<len; i++) {
  3660. 3660 : uid = uids[i];
  3661. 3661 : if (rows[uid])
  3662. 3662 : this.set_message(uid, 'unread', false);
  3663. 3663 : }
  3664. 3664 : };
  3665. 3665 :
  3666. 3666 : // Converts array of message UIDs to comma-separated list for use in URL
  3667. 3667 : // with select_all mode checking
  3668. 3668 : this.uids_to_list = function(uids)
  3669. 3669 : {
  3670. 3670 : if (this.select_all_mode)
  3671. 3671 : return '*';
  3672. 3672 :
  3673. 3673 : // multi-folder list of uids cannot be passed as a string (#6845)
  3674. 3674 : if ($.isArray(uids) && (uids.length == 1 || String(uids[0]).indexOf('-') == -1))
  3675. 3675 : uids = uids.join(',');
  3676. 3676 :
  3677. 3677 : return uids;
  3678. 3678 : };
  3679. 3679 :
  3680. 3680 : // Sets title of the delete button
  3681. 3681 : this.set_button_titles = function()
  3682. 3682 : {
  3683. 3683 : var label = 'deletemessage';
  3684. 3684 :
  3685. 3685 : if (!this.env.flag_for_deletion
  3686. 3686 : && this.env.trash_mailbox && this.env.mailbox != this.env.trash_mailbox
  3687. 3687 : && (!this.env.delete_junk || !this.env.junk_mailbox || this.env.mailbox != this.env.junk_mailbox)
  3688. 3688 : )
  3689. 3689 : label = 'movemessagetotrash';
  3690. 3690 :
  3691. 3691 : this.set_alttext('delete', label);
  3692. 3692 : };
  3693. 3693 :
  3694. 3694 : // Initialize input element for list page jump
  3695. 3695 : this.init_pagejumper = function(element)
  3696. 3696 : {
  3697. 3697 : $(element).addClass('rcpagejumper')
  3698. 3698 : .on('focus', function(e) {
  3699. 3699 : // create and display popup with page selection
  3700. 3700 : var i, html = '';
  3701. 3701 :
  3702. 3702 : for (i = 1; i <= ref.env.pagecount; i++)
  3703. 3703 : html += '<li>' + i + '</li>';
  3704. 3704 :
  3705. 3705 : html = '<ul class="toolbarmenu menu">' + html + '</ul>';
  3706. 3706 :
  3707. 3707 : if (!ref.pagejump) {
  3708. 3708 : ref.pagejump = $('<div id="pagejump-selector" class="popupmenu"></div>')
  3709. 3709 : .appendTo(document.body)
  3710. 3710 : .on('click', 'li', function() {
  3711. 3711 : if (!ref.busy)
  3712. 3712 : $(element).val($(this).text()).change();
  3713. 3713 : });
  3714. 3714 : }
  3715. 3715 :
  3716. 3716 : if (ref.pagejump.data('count') != i)
  3717. 3717 : ref.pagejump.html(html);
  3718. 3718 :
  3719. 3719 : ref.pagejump.attr('rel', '#' + this.id).data('count', i);
  3720. 3720 :
  3721. 3721 : // display page selector
  3722. 3722 : ref.show_menu('pagejump-selector', true, e);
  3723. 3723 : $(this).keydown();
  3724. 3724 : })
  3725. 3725 : // keyboard navigation
  3726. 3726 : .on('keydown keyup click', function(e) {
  3727. 3727 : var current, selector = $('#pagejump-selector'),
  3728. 3728 : ul = $('ul', selector),
  3729. 3729 : list = $('li', ul),
  3730. 3730 : height = ul.height(),
  3731. 3731 : p = parseInt(this.value);
  3732. 3732 :
  3733. 3733 : if (e.which != 27 && e.which != 9 && e.which != 13 && !selector.is(':visible'))
  3734. 3734 : return ref.show_menu('pagejump-selector', true, e);
  3735. 3735 :
  3736. 3736 : if (e.type == 'keydown') {
  3737. 3737 : // arrow-down
  3738. 3738 : if (e.which == 40) {
  3739. 3739 : if (list.length > p)
  3740. 3740 : this.value = (p += 1);
  3741. 3741 : }
  3742. 3742 : // arrow-up
  3743. 3743 : else if (e.which == 38) {
  3744. 3744 : if (p > 1 && list.length > p - 1)
  3745. 3745 : this.value = (p -= 1);
  3746. 3746 : }
  3747. 3747 : // enter
  3748. 3748 : else if (e.which == 13) {
  3749. 3749 : return $(this).change();
  3750. 3750 : }
  3751. 3751 : // esc, tab
  3752. 3752 : else if (e.which == 27 || e.which == 9) {
  3753. 3753 : ref.hide_menu('pagejump-selector', e);
  3754. 3754 : return $(element).val(ref.env.current_page);
  3755. 3755 : }
  3756. 3756 : }
  3757. 3757 :
  3758. 3758 : $('li.selected', ul).removeClass('selected');
  3759. 3759 :
  3760. 3760 : if ((current = $(list[p - 1])).length) {
  3761. 3761 : current.addClass('selected');
  3762. 3762 : $('#pagejump-selector').scrollTop(((ul.height() / list.length) * (p - 1)) - selector.height() / 2);
  3763. 3763 : }
  3764. 3764 : })
  3765. 3765 : .on('change', function(e) {
  3766. 3766 : // go to specified page
  3767. 3767 : var p = parseInt(this.value);
  3768. 3768 : if (p && p != ref.env.current_page && !ref.busy) {
  3769. 3769 : ref.hide_menu('pagejump-selector', e);
  3770. 3770 : ref.list_page(p);
  3771. 3771 : }
  3772. 3772 : });
  3773. 3773 : };
  3774. 3774 :
  3775. 3775 : // Update page-jumper state on list updates
  3776. 3776 : this.update_pagejumper = function()
  3777. 3777 : {
  3778. 3778 : $('input.rcpagejumper').val(this.env.current_page).prop('disabled', this.env.pagecount < 2);
  3779. 3779 : };
  3780. 3780 :
  3781. 3781 : // check for mailvelope API
  3782. 3782 : this.check_mailvelope = function(action)
  3783. 3783 : {
  3784. 3784 : if (window.mailvelope) {
  3785. 3785 : this.mailvelope_load(action);
  3786. 3786 : }
  3787. 3787 : else {
  3788. 3788 : $(window).on('mailvelope', function() {
  3789. 3789 : ref.mailvelope_load(action);
  3790. 3790 : });
  3791. 3791 : }
  3792. 3792 : };
  3793. 3793 :
  3794. 3794 : // Load Mailvelope functionality (and initialize keyring if needed)
  3795. 3795 : this.mailvelope_load = function(action)
  3796. 3796 : {
  3797. 3797 : var keyring = this.env.mailvelope_main_keyring ? undefined : this.env.user_id,
  3798. 3798 : fn = function(kr) {
  3799. 3799 : ref.mailvelope_keyring = kr;
  3800. 3800 : ref.mailvelope_init(action, kr);
  3801. 3801 : };
  3802. 3802 :
  3803. 3803 : mailvelope.getVersion().then(function(v) {
  3804. 3804 : mailvelope.VERSION = v;
  3805. 3805 : mailvelope.VERSION_MAJOR = Math.floor(parseFloat(v));
  3806. 3806 : return mailvelope.getKeyring(keyring);
  3807. 3807 : }).then(fn, function(err) {
  3808. 3808 : if (keyring) {
  3809. 3809 : // attempt to create a new keyring for this app/user
  3810. 3810 : mailvelope.createKeyring(keyring).then(fn, function(err) {
  3811. 3811 : console.error(err);
  3812. 3812 : });
  3813. 3813 : } else {
  3814. 3814 : console.error(err);
  3815. 3815 : }
  3816. 3816 : });
  3817. 3817 : };
  3818. 3818 :
  3819. 3819 : // Initializes Mailvelope editor or display container
  3820. 3820 : this.mailvelope_init = function(action, keyring)
  3821. 3821 : {
  3822. 3822 : if (!window.mailvelope)
  3823. 3823 : return;
  3824. 3824 :
  3825. 3825 : if (action == 'show' || action == 'preview' || action == 'print') {
  3826. 3826 : // decrypt text body
  3827. 3827 : if (this.env.is_pgp_content) {
  3828. 3828 : var data = $(this.env.is_pgp_content).text();
  3829. 3829 : ref.mailvelope_display_container(this.env.is_pgp_content, data, keyring);
  3830. 3830 : }
  3831. 3831 : // load pgp/mime message and pass it to the mailvelope display container
  3832. 3832 : else if (this.env.pgp_mime_part) {
  3833. 3833 : var msgid = this.display_message('loadingdata', 'loading'),
  3834. 3834 : selector = this.env.pgp_mime_container;
  3835. 3835 :
  3836. 3836 : $.ajax({
  3837. 3837 : type: 'GET',
  3838. 3838 : url: this.url('get', { '_mbox': this.env.mailbox, '_uid': this.env.uid, '_part': this.env.pgp_mime_part }),
  3839. 3839 : error: function(o, status, err) {
  3840. 3840 : ref.http_error(o, status, err, msgid);
  3841. 3841 : },
  3842. 3842 : success: function(data) {
  3843. 3843 : ref.mailvelope_display_container(selector, data, keyring, msgid);
  3844. 3844 : }
  3845. 3845 : });
  3846. 3846 : }
  3847. 3847 : }
  3848. 3848 : else if (action == 'compose') {
  3849. 3849 : this.env.compose_commands.push('compose-encrypted');
  3850. 3850 :
  3851. 3851 : var sign_supported = mailvelope.VERSION_MAJOR >= 2;
  3852. 3852 : var is_html = $('[name="_is_html"]').val() > 0;
  3853. 3853 :
  3854. 3854 : if (sign_supported)
  3855. 3855 : this.env.compose_commands.push('compose-encrypted-signed');
  3856. 3856 :
  3857. 3857 : if (this.env.pgp_mime_message) {
  3858. 3858 : // fetch PGP/Mime part and open load into Mailvelope editor
  3859. 3859 : var lock = this.set_busy(true, this.get_label('loadingdata'));
  3860. 3860 :
  3861. 3861 : $.ajax({
  3862. 3862 : type: 'GET',
  3863. 3863 : url: this.url('get', this.env.pgp_mime_message),
  3864. 3864 : error: function(o, status, err) {
  3865. 3865 : ref.http_error(o, status, err, lock);
  3866. 3866 : ref.enable_command('compose-encrypted', !is_html);
  3867. 3867 : if (sign_supported)
  3868. 3868 : ref.enable_command('compose-encrypted-signed', !is_html);
  3869. 3869 : },
  3870. 3870 : success: function(data) {
  3871. 3871 : ref.set_busy(false, null, lock);
  3872. 3872 :
  3873. 3873 : if (is_html) {
  3874. 3874 : ref.command('toggle-editor', {html: false, noconvert: true});
  3875. 3875 : $('#' + ref.env.composebody).val('');
  3876. 3876 : }
  3877. 3877 :
  3878. 3878 : ref.compose_encrypted({ quotedMail: data });
  3879. 3879 : ref.enable_command('compose-encrypted', true);
  3880. 3880 : ref.enable_command('compose-encrypted-signed', false);
  3881. 3881 : }
  3882. 3882 : });
  3883. 3883 : }
  3884. 3884 : else {
  3885. 3885 : // enable encrypted compose toggle
  3886. 3886 : this.enable_command('compose-encrypted', !is_html);
  3887. 3887 : if (sign_supported)
  3888. 3888 : this.enable_command('compose-encrypted-signed', !is_html);
  3889. 3889 : }
  3890. 3890 :
  3891. 3891 : // make sure to disable encryption button after toggling editor into HTML mode
  3892. 3892 : this.addEventListener('actionafter', function(args) {
  3893. 3893 : if (args.ret && args.action == 'toggle-editor') {
  3894. 3894 : ref.enable_command('compose-encrypted', !args.props.html);
  3895. 3895 : if (sign_supported)
  3896. 3896 : ref.enable_command('compose-encrypted-signed', !args.props.html);
  3897. 3897 : }
  3898. 3898 : });
  3899. 3899 : } else if (action == 'edit-identity') {
  3900. 3900 : ref.mailvelope_identity_keygen();
  3901. 3901 : }
  3902. 3902 : };
  3903. 3903 :
  3904. 3904 : // handler for the 'compose-encrypted-signed' command
  3905. 3905 : this.compose_encrypted_signed = function(props)
  3906. 3906 : {
  3907. 3907 : props = props || {};
  3908. 3908 : props.signMsg = true;
  3909. 3909 : this.compose_encrypted(props);
  3910. 3910 : };
  3911. 3911 :
  3912. 3912 : // handler for the 'compose-encrypted' command
  3913. 3913 : this.compose_encrypted = function(props)
  3914. 3914 : {
  3915. 3915 : var options, container = $('#' + this.env.composebody).parent();
  3916. 3916 :
  3917. 3917 : // remove Mailvelope editor if active
  3918. 3918 : if (ref.mailvelope_editor) {
  3919. 3919 : ref.mailvelope_editor = null;
  3920. 3920 : ref.set_button('compose-encrypted', 'act');
  3921. 3921 :
  3922. 3922 : container.removeClass('mailvelope').find('iframe:not([aria-hidden=true])').remove();
  3923. 3923 : $('#' + ref.env.composebody).show();
  3924. 3924 : $("[name='_pgpmime']").remove();
  3925. 3925 :
  3926. 3926 : // re-enable commands that operate on the compose body
  3927. 3927 : ref.enable_command('toggle-editor', 'insert-response', 'save-response', true);
  3928. 3928 : ref.enable_command('spellcheck', !!window.googie);
  3929. 3929 : ref.enable_command('insert-sig', !!(ref.env.signatures && ref.env.identity && ref.env.signatures[ref.env.identity]));
  3930. 3930 :
  3931. 3931 : ref.triggerEvent('compose-encrypted', { active:false });
  3932. 3932 : }
  3933. 3933 : // embed Mailvelope editor container
  3934. 3934 : else {
  3935. 3935 : if (this.spellcheck_state())
  3936. 3936 : this.editor.spellcheck_stop();
  3937. 3937 :
  3938. 3938 : if (props.quotedMail) {
  3939. 3939 : options = { quotedMail: props.quotedMail, quotedMailIndent: false };
  3940. 3940 : }
  3941. 3941 : else {
  3942. 3942 : options = { predefinedText: $('#' + this.env.composebody).val() };
  3943. 3943 : }
  3944. 3944 :
  3945. 3945 : if (props.signMsg) {
  3946. 3946 : options.signMsg = props.signMsg;
  3947. 3947 : }
  3948. 3948 :
  3949. 3949 : if (this.env.compose_mode == 'reply') {
  3950. 3950 : options.quotedMailIndent = true;
  3951. 3951 : options.quotedMailHeader = this.env.compose_reply_header;
  3952. 3952 : }
  3953. 3953 :
  3954. 3954 : mailvelope.createEditorContainer('#' + container.attr('id'), ref.mailvelope_keyring, options).then(function(editor) {
  3955. 3955 : ref.mailvelope_editor = editor;
  3956. 3956 : ref.set_button('compose-encrypted', 'sel');
  3957. 3957 :
  3958. 3958 : container.addClass('mailvelope');
  3959. 3959 : $('#' + ref.env.composebody).hide();
  3960. 3960 :
  3961. 3961 : // disable commands that operate on the compose body
  3962. 3962 : ref.enable_command('spellcheck', 'insert-sig', 'toggle-editor', 'insert-response', 'save-response', false);
  3963. 3963 : ref.triggerEvent('compose-encrypted', { active:true });
  3964. 3964 :
  3965. 3965 : if (!$.isEmptyObject(ref.env.attachments)) {
  3966. 3966 : // notify user if losing attachments
  3967. 3967 : if (ref.env.compose_mode != 'draft'
  3968. 3968 : || Object.keys(ref.env.attachments).length != 1
  3969. 3969 : || ref.env.attachments[Object.keys(ref.env.attachments)[0]].name != 'encrypted.asc'
  3970. 3970 : ) {
  3971. 3971 : ref.alert_dialog(ref.get_label('encryptnoattachments'));
  3972. 3972 : }
  3973. 3973 :
  3974. 3974 : $.each(ref.env.attachments, function(name, attach) {
  3975. 3975 : ref.remove_from_attachment_list(name);
  3976. 3976 : });
  3977. 3977 : }
  3978. 3978 : }, function(err) {
  3979. 3979 : console.error(err);
  3980. 3980 : console.log(options);
  3981. 3981 : });
  3982. 3982 : }
  3983. 3983 : };
  3984. 3984 :
  3985. 3985 : // callback to replace the message body with the full armored
  3986. 3986 : this.mailvelope_submit_messageform = function(draft, saveonly)
  3987. 3987 : {
  3988. 3988 : // get recipients
  3989. 3989 : var recipients = [];
  3990. 3990 : $.each(['to', 'cc', 'bcc'], function(i,field) {
  3991. 3991 : var pos, rcpt, val = $('[name="_' + field + '"]').val().trim();
  3992. 3992 : while (val.length && rcube_check_email(val, true)) {
  3993. 3993 : rcpt = RegExp.$2.replace(/^<+/, '').replace(/>+$/, '');
  3994. 3994 : recipients.push(rcpt);
  3995. 3995 : val = val.substr(val.indexOf(rcpt) + rcpt.length + 1).replace(/^\s*,\s*/, '');
  3996. 3996 : }
  3997. 3997 : });
  3998. 3998 :
  3999. 3999 : // check if we have keys for all recipients
  4000. 4000 : var isvalid = recipients.length > 0;
  4001. 4001 : ref.mailvelope_keyring.validKeyForAddress(recipients).then(function(status) {
  4002. 4002 : var missing_keys = [];
  4003. 4003 : $.each(status, function(k,v) {
  4004. 4004 : if (v === false) {
  4005. 4005 : isvalid = false;
  4006. 4006 : missing_keys.push(k);
  4007. 4007 : }
  4008. 4008 : });
  4009. 4009 :
  4010. 4010 : // list recipients with missing keys
  4011. 4011 : if (!isvalid && missing_keys.length) {
  4012. 4012 : // display dialog with missing keys
  4013. 4013 : ref.simple_dialog(
  4014. 4014 : ref.get_label('nopubkeyfor').replace('$email', missing_keys.join(', ')) +
  4015. 4015 : '<p>' + ref.get_label('searchpubkeyservers') + '</p>',
  4016. 4016 : 'encryptedsendialog',
  4017. 4017 : function() {
  4018. 4018 : ref.mailvelope_search_pubkeys(missing_keys, function() {
  4019. 4019 : return true; // close dialog
  4020. 4020 : });
  4021. 4021 : },
  4022. 4022 : {button: 'search'}
  4023. 4023 : );
  4024. 4024 : return false;
  4025. 4025 : }
  4026. 4026 :
  4027. 4027 : if (!isvalid) {
  4028. 4028 : if (!recipients.length) {
  4029. 4029 : ref.alert_dialog(ref.get_label('norecipientwarning'), function() {
  4030. 4030 : $("[name='_to']").focus();
  4031. 4031 : });
  4032. 4032 : }
  4033. 4033 : return false;
  4034. 4034 : }
  4035. 4035 :
  4036. 4036 : // add sender identity to recipients to be able to decrypt our very own message
  4037. 4037 : var senders = [], selected_sender = ref.env.identities[$("[name='_from'] option:selected").val()];
  4038. 4038 : $.each(ref.env.identities, function(k, sender) {
  4039. 4039 : senders.push(sender.email);
  4040. 4040 : });
  4041. 4041 :
  4042. 4042 : ref.mailvelope_keyring.validKeyForAddress(senders).then(function(status) {
  4043. 4043 : valid_sender = null;
  4044. 4044 : $.each(status, function(k,v) {
  4045. 4045 : if (v !== false) {
  4046. 4046 : valid_sender = k;
  4047. 4047 : if (valid_sender == selected_sender) {
  4048. 4048 : return false; // break
  4049. 4049 : }
  4050. 4050 : }
  4051. 4051 : });
  4052. 4052 :
  4053. 4053 : if (!valid_sender) {
  4054. 4054 : if (!confirm(ref.get_label('nopubkeyforsender'))) {
  4055. 4055 : return false;
  4056. 4056 : }
  4057. 4057 : }
  4058. 4058 :
  4059. 4059 : recipients.push(valid_sender);
  4060. 4060 :
  4061. 4061 : ref.mailvelope_editor.encrypt(recipients).then(function(armored) {
  4062. 4062 : // all checks passed, send message
  4063. 4063 : var form = ref.gui_objects.messageform,
  4064. 4064 : hidden = $("[name='_pgpmime']", form),
  4065. 4065 : msgid = ref.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage');
  4066. 4066 :
  4067. 4067 : form.target = ref.get_save_target(msgid);
  4068. 4068 : form._draft.value = draft ? '1' : '';
  4069. 4069 : form.action = ref.add_url(form.action, '_unlock', msgid);
  4070. 4070 : form.action = ref.add_url(form.action, '_framed', 1);
  4071. 4071 :
  4072. 4072 : if (saveonly) {
  4073. 4073 : form.action = ref.add_url(form.action, '_saveonly', 1);
  4074. 4074 : }
  4075. 4075 :
  4076. 4076 : // send pgp content via hidden field
  4077. 4077 : if (!hidden.length) {
  4078. 4078 : hidden = $('<input type="hidden" name="_pgpmime">').appendTo(form);
  4079. 4079 : }
  4080. 4080 : hidden.val(armored);
  4081. 4081 :
  4082. 4082 : form.submit();
  4083. 4083 :
  4084. 4084 : }, function(err) {
  4085. 4085 : console.log(err);
  4086. 4086 : }); // mailvelope_editor.encrypt()
  4087. 4087 :
  4088. 4088 : }, function(err) {
  4089. 4089 : console.error(err);
  4090. 4090 : }); // mailvelope_keyring.validKeyForAddress(senders)
  4091. 4091 :
  4092. 4092 : }, function(err) {
  4093. 4093 : console.error(err);
  4094. 4094 : }); // mailvelope_keyring.validKeyForAddress(recipients)
  4095. 4095 :
  4096. 4096 : return false;
  4097. 4097 : };
  4098. 4098 :
  4099. 4099 : // wrapper for the mailvelope.createDisplayContainer API call
  4100. 4100 : this.mailvelope_display_container = function(selector, data, keyring, msgid)
  4101. 4101 : {
  4102. 4102 : var error_handler = function(error) {
  4103. 4103 : // remove mailvelope frame with the error message
  4104. 4104 : $(selector + ' > iframe').remove();
  4105. 4105 : ref.hide_message(msgid);
  4106. 4106 : ref.display_message(error.message, 'error');
  4107. 4107 : };
  4108. 4108 :
  4109. 4109 : mailvelope.createDisplayContainer(selector, data, keyring, { senderAddress: this.env.sender }).then(function(status) {
  4110. 4110 : if (status.error && status.error.message) {
  4111. 4111 : return error_handler(status.error);
  4112. 4112 : }
  4113. 4113 :
  4114. 4114 : ref.hide_message(msgid);
  4115. 4115 : $(selector).children().not('iframe').hide();
  4116. 4116 : $(ref.gui_objects.messagebody).addClass('mailvelope');
  4117. 4117 :
  4118. 4118 : // on success we can remove encrypted part from the attachments list
  4119. 4119 : if (ref.env.pgp_mime_part)
  4120. 4120 : $('#attach' + ref.env.pgp_mime_part).remove();
  4121. 4121 :
  4122. 4122 : setTimeout(function() { $(window).resize(); }, 10);
  4123. 4123 : }, error_handler);
  4124. 4124 : };
  4125. 4125 :
  4126. 4126 : // subroutine to query keyservers for public keys
  4127. 4127 : this.mailvelope_search_pubkeys = function(emails, resolve, import_handler)
  4128. 4128 : {
  4129. 4129 : // query with publickey.js
  4130. 4130 : var deferred = [],
  4131. 4131 : pk = new PublicKey(this.env.keyservers),
  4132. 4132 : lock = ref.display_message('', 'loading');
  4133. 4133 :
  4134. 4134 : $.each(emails, function(i, email) {
  4135. 4135 : var d = $.Deferred();
  4136. 4136 : pk.search(email, function(results, errorCode) {
  4137. 4137 : if (errorCode !== null) {
  4138. 4138 : // rejecting would make all fail
  4139. 4139 : // d.reject(email);
  4140. 4140 : d.resolve([email]);
  4141. 4141 : }
  4142. 4142 : else {
  4143. 4143 : d.resolve([email].concat(results));
  4144. 4144 : }
  4145. 4145 : });
  4146. 4146 : deferred.push(d);
  4147. 4147 : });
  4148. 4148 :
  4149. 4149 : $.when.apply($, deferred).then(function() {
  4150. 4150 : var missing_keys = [],
  4151. 4151 : key_selection = [];
  4152. 4152 :
  4153. 4153 : // analyze results of all queries
  4154. 4154 : $.each(arguments, function(i, result) {
  4155. 4155 : var email = result.shift();
  4156. 4156 : if (!result.length) {
  4157. 4157 : missing_keys.push(email);
  4158. 4158 : }
  4159. 4159 : else {
  4160. 4160 : key_selection = key_selection.concat(result);
  4161. 4161 : }
  4162. 4162 : });
  4163. 4163 :
  4164. 4164 : ref.hide_message(lock);
  4165. 4165 : resolve(true);
  4166. 4166 :
  4167. 4167 : // show key import dialog
  4168. 4168 : if (key_selection.length) {
  4169. 4169 : ref.mailvelope_key_import_dialog(key_selection, import_handler);
  4170. 4170 : }
  4171. 4171 : // some keys could not be found
  4172. 4172 : if (missing_keys.length) {
  4173. 4173 : ref.display_message(ref.get_label('nopubkeyfor').replace('$email', missing_keys.join(', ')), 'warning');
  4174. 4174 : }
  4175. 4175 : }).fail(function() {
  4176. 4176 : console.error('Pubkey lookup failed with', arguments);
  4177. 4177 : ref.hide_message(lock);
  4178. 4178 : ref.display_message('pubkeysearcherror', 'error');
  4179. 4179 : resolve(false);
  4180. 4180 : });
  4181. 4181 : };
  4182. 4182 :
  4183. 4183 : // list the given public keys in a dialog with options to import
  4184. 4184 : // them into the local Mailvelope keyring
  4185. 4185 : this.mailvelope_key_import_dialog = function(candidates, import_handler)
  4186. 4186 : {
  4187. 4187 : var ul = $('<div>').addClass('listing pgpkeyimport');
  4188. 4188 :
  4189. 4189 : $.each(candidates, function(i, keyrec) {
  4190. 4190 : var li = $('<div>').addClass('key');
  4191. 4191 :
  4192. 4192 : if (keyrec.revoked) li.addClass('revoked');
  4193. 4193 : if (keyrec.disabled) li.addClass('disabled');
  4194. 4194 : if (keyrec.expired) li.addClass('expired');
  4195. 4195 :
  4196. 4196 : li.append($('<label>').addClass('keyid').text(ref.get_label('keyid')));
  4197. 4197 : li.append($('<a>').text(keyrec.keyid.substr(-8).toUpperCase())
  4198. 4198 : .attr({href: keyrec.info, target: '_blank', tabindex: '-1'}));
  4199. 4199 :
  4200. 4200 : li.append($('<label>').addClass('keylen').text(ref.get_label('keylength')));
  4201. 4201 : li.append($('<span>').text(keyrec.keylen));
  4202. 4202 :
  4203. 4203 : if (keyrec.expirationdate) {
  4204. 4204 : li.append($('<label>').addClass('keyexpired').text(ref.get_label('keyexpired')));
  4205. 4205 : li.append($('<span>').text(new Date(keyrec.expirationdate * 1000).toDateString()));
  4206. 4206 : }
  4207. 4207 :
  4208. 4208 : if (keyrec.revoked) {
  4209. 4209 : li.append($('<span>').addClass('keyrevoked').text(ref.get_label('keyrevoked')));
  4210. 4210 : }
  4211. 4211 :
  4212. 4212 : var ul_ = $('<ul>').addClass('uids');
  4213. 4213 : $.each(keyrec.uids, function(j, uid) {
  4214. 4214 : var li_ = $('<li>').addClass('uid');
  4215. 4215 : if (uid.revoked) li_.addClass('revoked');
  4216. 4216 : if (uid.disabled) li_.addClass('disabled');
  4217. 4217 : if (uid.expired) li_.addClass('expired');
  4218. 4218 :
  4219. 4219 : ul_.append(li_.text(uid.uid));
  4220. 4220 : });
  4221. 4221 :
  4222. 4222 : li.append(ul_);
  4223. 4223 : li.append($('<button>')
  4224. 4224 : .attr('rel', keyrec.keyid)
  4225. 4225 : .text(ref.get_label('import'))
  4226. 4226 : .addClass('button import importkey')
  4227. 4227 : .prop('disabled', keyrec.revoked || keyrec.disabled || keyrec.expired));
  4228. 4228 :
  4229. 4229 : ul.append(li);
  4230. 4230 : });
  4231. 4231 :
  4232. 4232 : // display dialog with missing keys
  4233. 4233 : ref.simple_dialog(
  4234. 4234 : $('<div>')
  4235. 4235 : .append($('<p>').html(ref.get_label('encryptpubkeysfound')))
  4236. 4236 : .append(ul),
  4237. 4237 : ref.get_label('importpubkeys'),
  4238. 4238 : null,
  4239. 4239 : {cancel_label: 'close', cancel_button: 'close'}
  4240. 4240 : );
  4241. 4241 :
  4242. 4242 : // delegate handler for import button clicks
  4243. 4243 : ul.on('click', 'button.importkey', function() {
  4244. 4244 : var btn = $(this),
  4245. 4245 : keyid = btn.attr('rel'),
  4246. 4246 : pk = new PublicKey(ref.env.keyservers),
  4247. 4247 : lock = ref.display_message('', 'loading');
  4248. 4248 :
  4249. 4249 : // fetch from keyserver and import to Mailvelope keyring
  4250. 4250 : pk.get(keyid, function(armored, errorCode) {
  4251. 4251 : ref.hide_message(lock);
  4252. 4252 :
  4253. 4253 : if (errorCode) {
  4254. 4254 : ref.display_message('keyservererror', 'error');
  4255. 4255 : return;
  4256. 4256 : }
  4257. 4257 :
  4258. 4258 : if (import_handler) {
  4259. 4259 : import_handler(armored);
  4260. 4260 : return;
  4261. 4261 : }
  4262. 4262 :
  4263. 4263 : // import to keyring
  4264. 4264 : ref.mailvelope_keyring.importPublicKey(armored).then(function(status) {
  4265. 4265 : if (status === 'REJECTED') {
  4266. 4266 : // ref.alert_dialog(ref.get_label('Key import was rejected'));
  4267. 4267 : }
  4268. 4268 : else {
  4269. 4269 : var $key = keyid.substr(-8).toUpperCase();
  4270. 4270 : btn.closest('.key').fadeOut();
  4271. 4271 : ref.display_message(ref.get_label('keyimportsuccess').replace('$key', $key), 'confirmation');
  4272. 4272 : }
  4273. 4273 : }, function(err) {
  4274. 4274 : console.log(err);
  4275. 4275 : });
  4276. 4276 : });
  4277. 4277 : });
  4278. 4278 : };
  4279. 4279 :
  4280. 4280 : // enable key management for identity
  4281. 4281 : this.mailvelope_identity_keygen = function()
  4282. 4282 : {
  4283. 4283 : var container = $(this.gui_objects.editform).find('.identity-encryption').first();
  4284. 4284 : var identity_email = $(this.gui_objects.editform).find('.ff_email').val().trim();
  4285. 4285 :
  4286. 4286 : if (!container.length || !identity_email || !this.mailvelope_keyring.createKeyGenContainer)
  4287. 4287 : return;
  4288. 4288 :
  4289. 4289 : var key_fingerprint;
  4290. 4290 : this.mailvelope_keyring.validKeyForAddress([identity_email])
  4291. 4291 : .then(function(keys) {
  4292. 4292 : var private_keys = [];
  4293. 4293 :
  4294. 4294 : if (keys && keys[identity_email] && Array.isArray(keys[identity_email].keys)) {
  4295. 4295 : var checks = [];
  4296. 4296 : for (var j=0; j < keys[identity_email].keys.length; j++) {
  4297. 4297 : checks.push((function(key) {
  4298. 4298 : return ref.mailvelope_keyring.hasPrivateKey(key.fingerprint)
  4299. 4299 : .then(function(found) {
  4300. 4300 : if (found) {
  4301. 4301 : private_keys.push(key);
  4302. 4302 : }
  4303. 4303 : });
  4304. 4304 : })(keys[identity_email].keys[j]));
  4305. 4305 : }
  4306. 4306 : return Promise.all(checks)
  4307. 4307 : .then(function() {
  4308. 4308 : return private_keys;
  4309. 4309 : });
  4310. 4310 : }
  4311. 4311 :
  4312. 4312 : return private_keys;
  4313. 4313 : }).then(function(private_keys) {
  4314. 4314 : var content = container.find('.identity-encryption-block').empty();
  4315. 4315 : if (private_keys && private_keys.length) {
  4316. 4316 : // show private key information
  4317. 4317 : $('<p>').text(ref.get_label('encryptionprivkeysinmailvelope').replace('$nr', private_keys.length)).appendTo(content);
  4318. 4318 : var ul = $('<ul>').addClass('keylist').appendTo(content);
  4319. 4319 : $.each(private_keys, function(i, key) {
  4320. 4320 : $('<li>').appendTo(ul)
  4321. 4321 : .append($('<strong>').addClass('fingerprint').text(String(key.fingerprint).toUpperCase()))
  4322. 4322 : .append($('<span>').addClass('identity').text('<' + identity_email + '> '));
  4323. 4323 : });
  4324. 4324 : } else {
  4325. 4325 : $('<p>').text(ref.get_label('encryptionnoprivkeysinmailvelope')).appendTo(content);
  4326. 4326 : }
  4327. 4327 :
  4328. 4328 : // show button to create a new key
  4329. 4329 : $('<button>')
  4330. 4330 : .attr('type', 'button')
  4331. 4331 : .addClass('button create')
  4332. 4332 : .text(ref.get_label('encryptioncreatekey'))
  4333. 4333 : .appendTo(content)
  4334. 4334 : .on('click', function() { ref.mailvelope_show_keygen_container(content, identity_email); });
  4335. 4335 : $('<span>').addClass('space').html('&nbsp;').appendTo(content);
  4336. 4336 : $('<button>')
  4337. 4337 : .attr('type', 'button')
  4338. 4338 : .addClass('button settings')
  4339. 4339 : .text(ref.get_label('openmailvelopesettings'))
  4340. 4340 : .appendTo(content)
  4341. 4341 : .on('click', function() { ref.mailvelope_keyring.openSettings(); });
  4342. 4342 :
  4343. 4343 : container.show();
  4344. 4344 : ref.triggerEvent('identity-encryption-show', { container: container });
  4345. 4345 : })
  4346. 4346 : .catch(function(err) {
  4347. 4347 : console.error('Mailvelope keyring error', err);
  4348. 4348 : })
  4349. 4349 : };
  4350. 4350 :
  4351. 4351 : // start pgp key generation using Mailvelope
  4352. 4352 : this.mailvelope_show_keygen_container = function(container, identity_email)
  4353. 4353 : {
  4354. 4354 : var cid = new Date().getTime();
  4355. 4355 : var user_id = {email: identity_email, fullName: $(ref.gui_objects.editform).find('.ff_name').val().trim()};
  4356. 4356 : var options = {userIds: [user_id], keySize: this.env.mailvelope_keysize};
  4357. 4357 :
  4358. 4358 : $('<div>').attr('id', 'mailvelope-keygen-container-' + cid)
  4359. 4359 : .css({height: '245px', marginBottom: '10px'})
  4360. 4360 : .appendTo(container.empty());
  4361. 4361 :
  4362. 4362 : this.mailvelope_keyring.createKeyGenContainer('#mailvelope-keygen-container-' + cid, options)
  4363. 4363 : .then(function(generator) {
  4364. 4364 : if (generator instanceof Error) {
  4365. 4365 : throw generator;
  4366. 4366 : }
  4367. 4367 :
  4368. 4368 : // append button to start key generation
  4369. 4369 : $('<button>')
  4370. 4370 : .attr('type', 'button')
  4371. 4371 : .addClass('button mainaction generate')
  4372. 4372 : .text(ref.get_label('generate'))
  4373. 4373 : .appendTo(container)
  4374. 4374 : .on('click', function() {
  4375. 4375 : var btn = $(this).prop('disabled', true);
  4376. 4376 : generator.generate()
  4377. 4377 : .then(function(result) {
  4378. 4378 : if (typeof result === 'string' && result.indexOf('BEGIN PGP') > 0) {
  4379. 4379 : ref.display_message(ref.get_label('keypaircreatesuccess').replace('$identity', identity_email), 'confirmation');
  4380. 4380 : // reset keygen view
  4381. 4381 : ref.mailvelope_identity_keygen();
  4382. 4382 : }
  4383. 4383 : })
  4384. 4384 : .catch(function(err) {
  4385. 4385 : ref.display_message(err.message || 'errortitle', 'error');
  4386. 4386 : btn.prop('disabled', false);
  4387. 4387 : });
  4388. 4388 : });
  4389. 4389 :
  4390. 4390 : $('<span>').addClass('space').html('&nbsp;').appendTo(container);
  4391. 4391 :
  4392. 4392 : $('<button>')
  4393. 4393 : .attr('type', 'button')
  4394. 4394 : .addClass('button cancel')
  4395. 4395 : .text(ref.get_label('cancel'))
  4396. 4396 : .appendTo(container)
  4397. 4397 : .on('click', function() { ref.mailvelope_identity_keygen(); });
  4398. 4398 :
  4399. 4399 : ref.triggerEvent('identity-encryption-update', { container: container });
  4400. 4400 : })
  4401. 4401 : .catch(function(err) {
  4402. 4402 : ref.display_message('errortitle', 'error');
  4403. 4403 : // start over
  4404. 4404 : ref.mailvelope_identity_keygen();
  4405. 4405 : });
  4406. 4406 : };
  4407. 4407 :
  4408. 4408 : this.mdn_request_dialog = function(uid, mailbox)
  4409. 4409 : {
  4410. 4410 : var props = {
  4411. 4411 : action: 'mark',
  4412. 4412 : data: { _uid: uid, _mbox: mailbox, _flag: 'mdnsent' }
  4413. 4413 : },
  4414. 4414 : buttons = [
  4415. 4415 : {
  4416. 4416 : text: this.get_label('send'),
  4417. 4417 : 'class': 'mainaction send',
  4418. 4418 : click: function(e, ui, dialog) {
  4419. 4419 : props.action = 'sendmdn';
  4420. 4420 : (ref.is_framed() ? parent.$ : $)(dialog || this).dialog('close');
  4421. 4421 : }
  4422. 4422 : },
  4423. 4423 : {
  4424. 4424 : text: this.get_label('ignore'),
  4425. 4425 : 'class': 'cancel',
  4426. 4426 : click: function(e, ui, dialog) {
  4427. 4427 : (ref.is_framed() ? parent.$ : $)(dialog || this).dialog('close');
  4428. 4428 : }
  4429. 4429 : }
  4430. 4430 : ],
  4431. 4431 : mdn_func = function(event, ui) {
  4432. 4432 : ref.http_post(props.action, props.data);
  4433. 4433 : // from default close function
  4434. 4434 : $(this).remove();
  4435. 4435 : };
  4436. 4436 :
  4437. 4437 : if (this.env.mdn_request_save) {
  4438. 4438 : buttons.unshift({
  4439. 4439 : text: this.get_label('sendalwaysto').replace('$email', this.env.mdn_request_sender.mailto),
  4440. 4440 : 'class': 'mainaction send',
  4441. 4441 : click: function(e, ui, dialog) {
  4442. 4442 : props.data._save = ref.env.mdn_request_save;
  4443. 4443 : props.data._address = ref.env.mdn_request_sender.string;
  4444. 4444 : $(e.target).next().click();
  4445. 4445 : }
  4446. 4446 : });
  4447. 4447 : }
  4448. 4448 :
  4449. 4449 : this.show_popup_dialog(this.get_label('mdnrequest'), this.get_label('sendreceipt'), buttons, { close: mdn_func });
  4450. 4450 : };
  4451. 4451 :
  4452. 4452 :
  4453. 4453 : /*********************************************************/
  4454. 4454 : /********* mailbox folders methods *********/
  4455. 4455 : /*********************************************************/
  4456. 4456 :
  4457. 4457 : this.expunge_mailbox = function(mbox)
  4458. 4458 : {
  4459. 4459 : var lock, post_data = {_mbox: mbox};
  4460. 4460 :
  4461. 4461 : // lock interface if it's the active mailbox
  4462. 4462 : if (mbox == this.env.mailbox) {
  4463. 4463 : lock = this.set_busy(true, 'loading');
  4464. 4464 : post_data._reload = 1;
  4465. 4465 : if (this.env.search_request)
  4466. 4466 : post_data._search = this.env.search_request;
  4467. 4467 : }
  4468. 4468 :
  4469. 4469 : // send request to server
  4470. 4470 : this.http_post('expunge', post_data, lock);
  4471. 4471 : };
  4472. 4472 :
  4473. 4473 : this.purge_mailbox = function(mbox)
  4474. 4474 : {
  4475. 4475 : this.confirm_dialog(this.get_label('purgefolderconfirm'), 'delete', function() {
  4476. 4476 : var lock, post_data = {_mbox: mbox};
  4477. 4477 :
  4478. 4478 : // lock interface if it's the active mailbox
  4479. 4479 : if (mbox == ref.env.mailbox) {
  4480. 4480 : lock = ref.set_busy(true, 'loading');
  4481. 4481 : post_data._reload = 1;
  4482. 4482 : }
  4483. 4483 :
  4484. 4484 : // send request to server
  4485. 4485 : ref.http_post('purge', post_data, lock);
  4486. 4486 : });
  4487. 4487 :
  4488. 4488 : return false;
  4489. 4489 : };
  4490. 4490 :
  4491. 4491 : // Mark all messages as read in:
  4492. 4492 : // - selected folder (mode=cur)
  4493. 4493 : // - selected folder and its subfolders (mode=sub)
  4494. 4494 : // - all folders (mode=all)
  4495. 4495 : this.mark_all_read = function(mbox, mode)
  4496. 4496 : {
  4497. 4497 : var state, content, nodes = [],
  4498. 4498 : list = this.message_list,
  4499. 4499 : folder = mbox || this.env.mailbox,
  4500. 4500 : post_data = {_uid: '*', _flag: 'read', _mbox: folder, _folders: mode};
  4501. 4501 :
  4502. 4502 : if (typeof mode != 'string') {
  4503. 4503 : state = this.mark_all_read_state(folder);
  4504. 4504 : if (!state)
  4505. 4505 : return;
  4506. 4506 :
  4507. 4507 : if (state > 1) {
  4508. 4508 : // build content of the dialog
  4509. 4509 : $.each({cur: 1, sub: 2, all: 4}, function(i, v) {
  4510. 4510 : var id = 'readallmode' + i,
  4511. 4511 : label = $('<label>').attr('for', id).text(ref.get_label('folders-' + i)),
  4512. 4512 : input = $('<input>').attr({type: 'radio', value: i, name: 'mode', id: id, disabled: !(state & v)});
  4513. 4513 :
  4514. 4514 : nodes.push($('<li>').append([input, label]));
  4515. 4515 : });
  4516. 4516 :
  4517. 4517 : content = $('<ul class="proplist">').append(nodes);
  4518. 4518 : $('input:not([disabled])', content).first().attr('checked', true);
  4519. 4519 :
  4520. 4520 : this.simple_dialog(content, this.get_label('markallread'),
  4521. 4521 : function() {
  4522. 4522 : ref.mark_all_read(folder, $('input:checked', content).val());
  4523. 4523 : return true;
  4524. 4524 : },
  4525. 4525 : {button: 'mark', button_class: 'save'}
  4526. 4526 : );
  4527. 4527 :
  4528. 4528 : return;
  4529. 4529 : }
  4530. 4530 :
  4531. 4531 : post_data._folders = 'cur'; // only current folder has unread messages
  4532. 4532 : }
  4533. 4533 :
  4534. 4534 : // mark messages on the list
  4535. 4535 : $.each(list ? list.rows : [], function(uid, row) {
  4536. 4536 : if (!row.unread)
  4537. 4537 : return;
  4538. 4538 :
  4539. 4539 : var mbox = ref.env.messages[uid].mbox;
  4540. 4540 : if (mode == 'all' || mbox == ref.env.mailbox
  4541. 4541 : || (mode == 'sub' && mbox.startsWith(ref.env.mailbox + ref.env.delimiter))
  4542. 4542 : ) {
  4543. 4543 : ref.set_message(uid, 'unread', false);
  4544. 4544 : }
  4545. 4545 : });
  4546. 4546 :
  4547. 4547 : // send the request
  4548. 4548 : this.http_post('mark', post_data, this.display_message('markingmessage', 'loading'));
  4549. 4549 : };
  4550. 4550 :
  4551. 4551 : // Enable/disable mark-all-read action depending on folders state
  4552. 4552 : this.mark_all_read_state = function(mbox)
  4553. 4553 : {
  4554. 4554 : var state = 0,
  4555. 4555 : li = this.treelist.get_item(mbox || this.env.mailbox),
  4556. 4556 : folder_item = $(li).is('.unread') ? 1 : 0,
  4557. 4557 : subfolder_items = $('li.unread', li).length,
  4558. 4558 : all_items = $('li.unread', ref.gui_objects.folderlist).length;
  4559. 4559 :
  4560. 4560 : state += folder_item;
  4561. 4561 : state += subfolder_items ? 2 : 0;
  4562. 4562 : state += all_items > folder_item + subfolder_items ? 4 : 0;
  4563. 4563 :
  4564. 4564 : this.enable_command('mark-all-read', state > 0);
  4565. 4565 :
  4566. 4566 : return state;
  4567. 4567 : };
  4568. 4568 :
  4569. 4569 : // Display "bounce message" dialog
  4570. 4570 : this.bounce = function(props, obj, event)
  4571. 4571 : {
  4572. 4572 : // get message uid and folder
  4573. 4573 : var uid = this.get_single_uid(),
  4574. 4574 : url = this.url('bounce', {_framed: 1, _uid: uid, _mbox: this.get_message_mailbox(uid)}),
  4575. 4575 : dialog = $('<iframe>').attr('src', url),
  4576. 4576 : get_form = function() {
  4577. 4577 : var rc = $('iframe', dialog)[0].contentWindow.rcmail;
  4578. 4578 : return {rc: rc, form: rc.gui_objects.messageform};
  4579. 4579 : },
  4580. 4580 : post_func = function() {
  4581. 4581 : var post = {}, form = get_form();
  4582. 4582 :
  4583. 4583 : $.each($(form.form).serializeArray(), function() { post[this.name] = this.value; });
  4584. 4584 :
  4585. 4585 : post._uid = form.rc.env.uid;
  4586. 4586 : post._mbox = form.rc.env.mailbox;
  4587. 4587 : delete post._action;
  4588. 4588 : delete post._task;
  4589. 4589 :
  4590. 4590 : if (post._to || post._cc || post._bcc) {
  4591. 4591 : ref.http_post('bounce', post, ref.set_busy(true, 'sendingmessage'));
  4592. 4592 : dialog.dialog('close');
  4593. 4593 : }
  4594. 4594 : },
  4595. 4595 : submit_func = function() {
  4596. 4596 : var form = get_form();
  4597. 4597 :
  4598. 4598 : if (typeof form.form != 'object')
  4599. 4599 : return false;
  4600. 4600 :
  4601. 4601 : if (!form.rc.check_compose_address_fields(post_func, form.form))
  4602. 4602 : return false;
  4603. 4603 :
  4604. 4604 : return post_func();
  4605. 4605 : };
  4606. 4606 :
  4607. 4607 : this.hide_menu('forwardmenu', event);
  4608. 4608 :
  4609. 4609 : dialog = this.simple_dialog(dialog, this.gettext('bouncemsg'), submit_func, {
  4610. 4610 : button: 'bounce',
  4611. 4611 : width: 400,
  4612. 4612 : height: 300
  4613. 4613 : });
  4614. 4614 :
  4615. 4615 : return true;
  4616. 4616 : };
  4617. 4617 :
  4618. 4618 :
  4619. 4619 : /*********************************************************/
  4620. 4620 : /********* message compose methods *********/
  4621. 4621 : /*********************************************************/
  4622. 4622 :
  4623. 4623 : this.open_compose_step = function(p)
  4624. 4624 : {
  4625. 4625 : var url = this.url('mail/compose', p);
  4626. 4626 :
  4627. 4627 : // open new compose window
  4628. 4628 : if (this.env.compose_extwin && !this.env.extwin) {
  4629. 4629 : this.open_window(url);
  4630. 4630 : }
  4631. 4631 : else {
  4632. 4632 : this.redirect(url);
  4633. 4633 : if (this.env.extwin)
  4634. 4634 : window.resizeTo(Math.max(this.env.popup_width, $(window).width()), $(window).height() + 24);
  4635. 4635 : }
  4636. 4636 : };
  4637. 4637 :
  4638. 4638 : // init message compose form: set focus and eventhandlers
  4639. 4639 : this.init_messageform = function()
  4640. 4640 : {
  4641. 4641 : if (!this.gui_objects.messageform)
  4642. 4642 : return false;
  4643. 4643 :
  4644. 4644 : var elem, pos,
  4645. 4645 : input_from = $("[name='_from']"),
  4646. 4646 : input_to = $("[name='_to']"),
  4647. 4647 : input_subject = $("[name='_subject']"),
  4648. 4648 : input_message = $("[name='_message']").get(0),
  4649. 4649 : html_mode = $("[name='_is_html']").val() == '1',
  4650. 4650 : opener_rc = this.opener();
  4651. 4651 :
  4652. 4652 : // close compose step in opener
  4653. 4653 : if (opener_rc && opener_rc.env.action == 'compose') {
  4654. 4654 : setTimeout(function() {
  4655. 4655 : if (opener.history.length > 1)
  4656. 4656 : opener.history.back();
  4657. 4657 : else
  4658. 4658 : opener_rc.redirect(opener_rc.get_task_url('mail'));
  4659. 4659 : }, 100);
  4660. 4660 : this.env.opened_extwin = true;
  4661. 4661 : }
  4662. 4662 :
  4663. 4663 : if (!html_mode) {
  4664. 4664 : // On Back button Chrome will overwrite textarea with old content
  4665. 4665 : // causing e.g. the same signature is added twice (#5809)
  4666. 4666 : if (input_message.value && input_message.defaultValue !== undefined)
  4667. 4667 : input_message.value = input_message.defaultValue;
  4668. 4668 :
  4669. 4669 : pos = this.env.top_posting && this.env.compose_mode ? 0 : input_message.value.length;
  4670. 4670 :
  4671. 4671 : // add signature according to selected identity
  4672. 4672 : // if we have HTML editor, signature is added in a callback
  4673. 4673 : if (input_from.prop('type') == 'select-one') {
  4674. 4674 : // for some reason the caret initially is not at pos=0 in Firefox 51 (#5628)
  4675. 4675 : this.set_caret_pos(input_message, 0);
  4676. 4676 : this.change_identity(input_from[0]);
  4677. 4677 : }
  4678. 4678 :
  4679. 4679 : // set initial cursor position
  4680. 4680 : this.set_caret_pos(input_message, pos);
  4681. 4681 :
  4682. 4682 : // scroll to the bottom of the textarea (#1490114)
  4683. 4683 : if (pos) {
  4684. 4684 : $(input_message).scrollTop(input_message.scrollHeight);
  4685. 4685 : }
  4686. 4686 : }
  4687. 4687 :
  4688. 4688 : // check for locally stored compose data
  4689. 4689 : if (this.env.save_localstorage)
  4690. 4690 : this.compose_restore_dialog(0, html_mode)
  4691. 4691 :
  4692. 4692 : if (input_to.val() == '')
  4693. 4693 : elem = input_to;
  4694. 4694 : else if (input_subject.val() == '')
  4695. 4695 : elem = input_subject;
  4696. 4696 : else if (input_message)
  4697. 4697 : elem = input_message;
  4698. 4698 :
  4699. 4699 : this.env.compose_focus_elem = this.init_messageform_inputs(elem);
  4700. 4700 :
  4701. 4701 : // get summary of all field values
  4702. 4702 : this.compose_field_hash(true);
  4703. 4703 :
  4704. 4704 : // start the auto-save timer
  4705. 4705 : this.auto_save_start();
  4706. 4706 : };
  4707. 4707 :
  4708. 4708 : // init autocomplete events on compose form inputs
  4709. 4709 : this.init_messageform_inputs = function(focused)
  4710. 4710 : {
  4711. 4711 : var i,
  4712. 4712 : input_to = $("[name='_to']"),
  4713. 4713 : ac_fields = ['cc', 'bcc', 'replyto', 'followupto'];
  4714. 4714 :
  4715. 4715 : // init live search events
  4716. 4716 : this.init_address_input_events(input_to);
  4717. 4717 : for (i in ac_fields) {
  4718. 4718 : this.init_address_input_events($("[name='_"+ac_fields[i]+"']"));
  4719. 4719 : }
  4720. 4720 :
  4721. 4721 : if (!focused)
  4722. 4722 : focused = input_to;
  4723. 4723 :
  4724. 4724 : // focus first empty element (and return it)
  4725. 4725 : return $(focused).focus().get(0);
  4726. 4726 : };
  4727. 4727 :
  4728. 4728 : this.compose_restore_dialog = function(j, html_mode)
  4729. 4729 : {
  4730. 4730 : var i, key, formdata, index = this.local_storage_get_item('compose.index', []);
  4731. 4731 :
  4732. 4732 : var show_next = function(i) {
  4733. 4733 : if (++i < index.length)
  4734. 4734 : ref.compose_restore_dialog(i, html_mode)
  4735. 4735 : }
  4736. 4736 :
  4737. 4737 : for (i = j || 0; i < index.length; i++) {
  4738. 4738 : key = index[i];
  4739. 4739 : formdata = this.local_storage_get_item('compose.' + key, null, true);
  4740. 4740 : if (!formdata) {
  4741. 4741 : continue;
  4742. 4742 : }
  4743. 4743 : // restore saved copy of current compose_id
  4744. 4744 : if (formdata.changed && key == this.env.compose_id) {
  4745. 4745 : this.restore_compose_form(key, html_mode);
  4746. 4746 : break;
  4747. 4747 : }
  4748. 4748 : // skip records from 'other' drafts
  4749. 4749 : if (this.env.draft_id && formdata.draft_id && formdata.draft_id != this.env.draft_id) {
  4750. 4750 : continue;
  4751. 4751 : }
  4752. 4752 : // skip records on reply
  4753. 4753 : if (this.env.reply_msgid && formdata.reply_msgid != this.env.reply_msgid) {
  4754. 4754 : continue;
  4755. 4755 : }
  4756. 4756 : // show dialog asking to restore the message
  4757. 4757 : if (formdata.changed && formdata.session != this.env.session_id) {
  4758. 4758 : this.show_popup_dialog(
  4759. 4759 : this.get_label('restoresavedcomposedata')
  4760. 4760 : .replace('$date', new Date(formdata.changed).toLocaleString())
  4761. 4761 : .replace('$subject', formdata._subject)
  4762. 4762 : .replace(/\n/g, '<br/>'),
  4763. 4763 : this.get_label('restoremessage'),
  4764. 4764 : [{
  4765. 4765 : text: this.get_label('restore'),
  4766. 4766 : 'class': 'mainaction restore',
  4767. 4767 : click: function(){
  4768. 4768 : ref.restore_compose_form(key, html_mode);
  4769. 4769 : ref.remove_compose_data(key); // remove old copy
  4770. 4770 : ref.save_compose_form_local(); // save under current compose_id
  4771. 4771 : $(this).dialog('close');
  4772. 4772 : }
  4773. 4773 : },
  4774. 4774 : {
  4775. 4775 : text: this.get_label('delete'),
  4776. 4776 : 'class': 'delete',
  4777. 4777 : click: function(){
  4778. 4778 : ref.remove_compose_data(key);
  4779. 4779 : $(this).dialog('close');
  4780. 4780 : show_next(i);
  4781. 4781 : }
  4782. 4782 : },
  4783. 4783 : {
  4784. 4784 : text: this.get_label('ignore'),
  4785. 4785 : 'class': 'cancel',
  4786. 4786 : click: function(){
  4787. 4787 : $(this).dialog('close');
  4788. 4788 : show_next(i);
  4789. 4789 : }
  4790. 4790 : }]
  4791. 4791 : );
  4792. 4792 : break;
  4793. 4793 : }
  4794. 4794 : }
  4795. 4795 : }
  4796. 4796 :
  4797. 4797 : this.init_address_input_events = function(obj, props)
  4798. 4798 : {
  4799. 4799 : // configure parallel autocompletion
  4800. 4800 : if (!props && this.env.autocomplete_threads > 0) {
  4801. 4801 : props = {
  4802. 4802 : threads: this.env.autocomplete_threads,
  4803. 4803 : sources: this.env.autocomplete_sources
  4804. 4804 : };
  4805. 4805 : }
  4806. 4806 :
  4807. 4807 : obj.keydown(function(e) { return ref.ksearch_keydown(e, this, props); })
  4808. 4808 : .attr({autocomplete: 'off', 'aria-autocomplete': 'list', 'aria-expanded': 'false', role: 'combobox'});
  4809. 4809 :
  4810. 4810 : var callback = function(e) {
  4811. 4811 : if (ref.ksearch_pane && e.target === ref.ksearch_pane.get(0)) {
  4812. 4812 : return;
  4813. 4813 : }
  4814. 4814 : ref.ksearch_hide();
  4815. 4815 : };
  4816. 4816 :
  4817. 4817 : // hide the popup on any click
  4818. 4818 : $(document).on('click', callback);
  4819. 4819 : // and on scroll (that cannot be jQuery.on())
  4820. 4820 : document.addEventListener('scroll', callback, true);
  4821. 4821 : };
  4822. 4822 :
  4823. 4823 : this.submit_messageform = function(draft, saveonly)
  4824. 4824 : {
  4825. 4825 : var form = this.gui_objects.messageform;
  4826. 4826 :
  4827. 4827 : if (!form)
  4828. 4828 : return;
  4829. 4829 :
  4830. 4830 : // the message has been sent but not saved, ask the user what to do
  4831. 4831 : if (!saveonly && this.env.is_sent) {
  4832. 4832 : return this.simple_dialog(this.get_label('messageissent'), '', // TODO: dialog title
  4833. 4833 : function() {
  4834. 4834 : ref.submit_messageform(false, true);
  4835. 4835 : return true;
  4836. 4836 : }
  4837. 4837 : );
  4838. 4838 : }
  4839. 4839 :
  4840. 4840 : // delegate sending to Mailvelope routine
  4841. 4841 : if (this.mailvelope_editor) {
  4842. 4842 : return this.mailvelope_submit_messageform(draft, saveonly);
  4843. 4843 : }
  4844. 4844 :
  4845. 4845 : // all checks passed, send message
  4846. 4846 : var msgid = this.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage'),
  4847. 4847 : lang = this.spellcheck_lang(),
  4848. 4848 : files = [];
  4849. 4849 :
  4850. 4850 : // send files list
  4851. 4851 : $('li', this.gui_objects.attachmentlist).each(function() { files.push(this.id.replace(/^rcmfile/, '')); });
  4852. 4852 : $('[name="_attachments"]', form).val(files.join());
  4853. 4853 :
  4854. 4854 : form.target = this.get_save_target(msgid);
  4855. 4855 : form._draft.value = draft ? '1' : '';
  4856. 4856 : form.action = this.add_url(form.action, '_unlock', msgid);
  4857. 4857 : form.action = this.add_url(form.action, '_framed', 1);
  4858. 4858 :
  4859. 4859 : if (lang)
  4860. 4860 : form.action = this.add_url(form.action, '_lang', lang);
  4861. 4861 : if (saveonly)
  4862. 4862 : form.action = this.add_url(form.action, '_saveonly', 1);
  4863. 4863 :
  4864. 4864 : // register timer to notify about connection timeout
  4865. 4865 : this.submit_timer = setTimeout(function() {
  4866. 4866 : ref.set_busy(false, null, msgid);
  4867. 4867 : ref.display_message('requesttimedout', 'error');
  4868. 4868 : }, this.env.request_timeout * 1000);
  4869. 4869 :
  4870. 4870 : form.submit();
  4871. 4871 : };
  4872. 4872 :
  4873. 4873 : this.compose_recipient_select = function(list)
  4874. 4874 : {
  4875. 4875 : var id, n, recipients = 0, selection = list.get_selection();
  4876. 4876 : for (n=0; n < selection.length; n++) {
  4877. 4877 : id = selection[n];
  4878. 4878 : if (this.env.contactdata[id])
  4879. 4879 : recipients++;
  4880. 4880 : }
  4881. 4881 : this.enable_command('add-recipient', recipients);
  4882. 4882 : };
  4883. 4883 :
  4884. 4884 : this.compose_add_recipient = function(field)
  4885. 4885 : {
  4886. 4886 : // find last focused field name
  4887. 4887 : if (!field) {
  4888. 4888 : field = $(this.env.focused_field).filter(':visible');
  4889. 4889 : field = field.length ? field.attr('id').replace('_', '') : 'to';
  4890. 4890 : }
  4891. 4891 :
  4892. 4892 : var recipients = [], input = $('#_' + field), selection = this.contact_list.get_selection();
  4893. 4893 :
  4894. 4894 : if (this.contact_list && selection.length) {
  4895. 4895 : var data, name, n, id;
  4896. 4896 : for (n = 0; n < selection.length; n++) {
  4897. 4897 : if ((id = selection[n]) && (data = this.env.contactdata[id])) {
  4898. 4898 : name = data.name || data
  4899. 4899 :
  4900. 4900 : // group is added, expand it
  4901. 4901 : if (id.charAt(0) == 'E' && input.length) {
  4902. 4902 : // We wrap the group name with invisible markers to prevent from problems with group expanding (#7569)
  4903. 4903 : name = '\u200b' + name + '\u200b';
  4904. 4904 : var gid = id.substr(1);
  4905. 4905 : this.group2expand[gid] = {name: name, input: input.get(0)};
  4906. 4906 : this.http_request('group-expand', {_source: data.source || this.env.source, _gid: gid}, false);
  4907. 4907 : }
  4908. 4908 :
  4909. 4909 : recipients.push(name);
  4910. 4910 : }
  4911. 4911 : }
  4912. 4912 : }
  4913. 4913 :
  4914. 4914 : if (recipients.length && input.length) {
  4915. 4915 : var oldval = input.val();
  4916. 4916 : if (oldval && !/[,;]\s*$/.test(oldval))
  4917. 4917 : oldval += ', ';
  4918. 4918 : input.val(oldval + recipients.join(', ') + ', ').change();
  4919. 4919 : this.triggerEvent('add-recipient', { field:field, recipients:recipients });
  4920. 4920 : }
  4921. 4921 :
  4922. 4922 : return recipients.length;
  4923. 4923 : };
  4924. 4924 :
  4925. 4925 : // checks the input fields before sending a message
  4926. 4926 : this.check_compose_input = function(cmd)
  4927. 4927 : {
  4928. 4928 : var key,
  4929. 4929 : input_subject = $("[name='_subject']");
  4930. 4930 :
  4931. 4931 : // check if all files has been uploaded
  4932. 4932 : for (key in this.env.attachments) {
  4933. 4933 : if (typeof this.env.attachments[key] === 'object' && !this.env.attachments[key].complete) {
  4934. 4934 : this.alert_dialog(this.get_label('notuploadedwarning'));
  4935. 4935 : return false;
  4936. 4936 : }
  4937. 4937 : }
  4938. 4938 :
  4939. 4939 : // display localized warning for missing subject
  4940. 4940 : if (!this.env.nosubject_warned && input_subject.val() == '') {
  4941. 4941 : var dialog,
  4942. 4942 : prompt_value = $('<input>').attr({type: 'text', size: 40, 'data-submit': 'true'}),
  4943. 4943 : myprompt = $('<div class="prompt">')
  4944. 4944 : .append($('<p class="message">').text(this.get_label('nosubjectwarning')))
  4945. 4945 : .append(prompt_value),
  4946. 4946 : save_func = function() {
  4947. 4947 : input_subject.val(prompt_value.val());
  4948. 4948 : dialog.dialog('close');
  4949. 4949 : if (ref.check_compose_input(cmd))
  4950. 4950 : ref.command(cmd, { nocheck:true }); // repeat command which triggered this
  4951. 4951 : };
  4952. 4952 :
  4953. 4953 : dialog = this.show_popup_dialog(
  4954. 4954 : myprompt,
  4955. 4955 : this.get_label('nosubjecttitle'),
  4956. 4956 : [{
  4957. 4957 : text: this.get_label('sendmessage'),
  4958. 4958 : 'class': 'mainaction send',
  4959. 4959 : click: function() { save_func(); }
  4960. 4960 : }, {
  4961. 4961 : text: this.get_label('cancel'),
  4962. 4962 : 'class': 'cancel',
  4963. 4963 : click: function() {
  4964. 4964 : input_subject.focus();
  4965. 4965 : dialog.dialog('close');
  4966. 4966 : }
  4967. 4967 : }],
  4968. 4968 : {dialogClass: 'warning'}
  4969. 4969 : );
  4970. 4970 :
  4971. 4971 : this.env.nosubject_warned = true;
  4972. 4972 : return false;
  4973. 4973 : }
  4974. 4974 :
  4975. 4975 : // check for empty body (only possible if not mailvelope encrypted)
  4976. 4976 : if (!this.mailvelope_editor && !this.editor.get_content() && !confirm(this.get_label('nobodywarning'))) {
  4977. 4977 : this.editor.focus();
  4978. 4978 : return false;
  4979. 4979 : }
  4980. 4980 :
  4981. 4981 : if (!this.check_compose_address_fields(cmd))
  4982. 4982 : return false;
  4983. 4983 :
  4984. 4984 : // move body from html editor to textarea (just to be sure, #1485860)
  4985. 4985 : this.editor.save();
  4986. 4986 :
  4987. 4987 : return true;
  4988. 4988 : };
  4989. 4989 :
  4990. 4990 : this.check_compose_address_fields = function(cmd, form)
  4991. 4991 : {
  4992. 4992 : if (!form)
  4993. 4993 : form = window.document;
  4994. 4994 :
  4995. 4995 : // check input fields
  4996. 4996 : var key, recipients, dialog,
  4997. 4997 : limit = this.env.max_disclosed_recipients,
  4998. 4998 : input_to = $("[name='_to']", form),
  4999. 4999 : input_cc = $("[name='_cc']", form),
  5000. 5000 : input_bcc = $("[name='_bcc']", form),
  5001. 5001 : input_from = $("[name='_from']", form),
  5002. 5002 : get_recipients = function(fields) {
  5003. 5003 : fields = $.map(fields, function(v) {
  5004. 5004 : v = v.val().trim();
  5005. 5005 : return v.length ? v : null;
  5006. 5006 : });
  5007. 5007 : return fields.join(',').replace(/^[\s,;]+/, '').replace(/[\s,;]+$/, '');
  5008. 5008 : };
  5009. 5009 :
  5010. 5010 : // check sender (if have no identities)
  5011. 5011 : if (input_from.prop('type') == 'text' && !rcube_check_email(input_from.val(), true)) {
  5012. 5012 : this.alert_dialog(this.get_label('nosenderwarning'), function() {
  5013. 5013 : input_from.focus();
  5014. 5014 : });
  5015. 5015 : return false;
  5016. 5016 : }
  5017. 5017 :
  5018. 5018 : // check for empty recipient
  5019. 5019 : if (!rcube_check_email(get_recipients([input_to, input_cc, input_bcc]), true)) {
  5020. 5020 : this.alert_dialog(this.get_label('norecipientwarning'), function() {
  5021. 5021 : input_to.focus();
  5022. 5022 : });
  5023. 5023 : return false;
  5024. 5024 : }
  5025. 5025 :
  5026. 5026 : // check disclosed recipients limit
  5027. 5027 : if (limit && !this.env.disclosed_recipients_warned
  5028. 5028 : && rcube_check_email(recipients = get_recipients([input_to, input_cc]), true, true) > limit
  5029. 5029 : ) {
  5030. 5030 : var save_func = function(move_to_bcc) {
  5031. 5031 : if (move_to_bcc) {
  5032. 5032 : var bcc = input_bcc.val();
  5033. 5033 : input_bcc.val((bcc ? (bcc + ', ') : '') + recipients).change();
  5034. 5034 : input_to.val('').change();
  5035. 5035 : input_cc.val('').change();
  5036. 5036 : }
  5037. 5037 :
  5038. 5038 : dialog.dialog('close');
  5039. 5039 :
  5040. 5040 : if (typeof cmd == 'function')
  5041. 5041 : cmd();
  5042. 5042 : else if (cmd)
  5043. 5043 : ref.command(cmd, { nocheck:true }); // repeat command which triggered this
  5044. 5044 : };
  5045. 5045 :
  5046. 5046 : dialog = this.show_popup_dialog(
  5047. 5047 : this.get_label('disclosedrecipwarning'),
  5048. 5048 : this.get_label('disclosedreciptitle'),
  5049. 5049 : [{
  5050. 5050 : text: this.get_label('sendmessage'),
  5051. 5051 : click: function() { save_func(false); },
  5052. 5052 : 'class': 'mainaction'
  5053. 5053 : }, {
  5054. 5054 : text: this.get_label('bccinstead'),
  5055. 5055 : click: function() { save_func(true); }
  5056. 5056 : }, {
  5057. 5057 : text: this.get_label('cancel'),
  5058. 5058 : click: function() { dialog.dialog('close'); },
  5059. 5059 : 'class': 'cancel'
  5060. 5060 : }],
  5061. 5061 : {dialogClass: 'warning'}
  5062. 5062 : );
  5063. 5063 :
  5064. 5064 : this.env.disclosed_recipients_warned = true;
  5065. 5065 : return false;
  5066. 5066 : }
  5067. 5067 :
  5068. 5068 : return true;
  5069. 5069 : };
  5070. 5070 :
  5071. 5071 : this.toggle_editor = function(props, obj, e)
  5072. 5072 : {
  5073. 5073 : // @todo: this should work also with many editors on page
  5074. 5074 : var mode, result = this.editor.toggle(props.html, props.noconvert || false),
  5075. 5075 : control = $('#' + this.editor.id).data('control') || $(e ? e.target : []);
  5076. 5076 :
  5077. 5077 : if (result)
  5078. 5078 : mode = props.html ? 'html' : 'plain';
  5079. 5079 : else
  5080. 5080 : mode = props.html ? 'plain' : 'html';
  5081. 5081 :
  5082. 5082 : // update internal format flag
  5083. 5083 : $("[name='_is_html']").val(mode == 'html' ? 1 : 0);
  5084. 5084 :
  5085. 5085 : if (control.is('[type=checkbox]'))
  5086. 5086 : control.prop('checked', mode == 'html');
  5087. 5087 : else
  5088. 5088 : control.val(mode);
  5089. 5089 :
  5090. 5090 : return result;
  5091. 5091 : };
  5092. 5092 :
  5093. 5093 : // Inserts a predefined response to the compose editor
  5094. 5094 : this.insert_response = function(key)
  5095. 5095 : {
  5096. 5096 : this.editor.replace(this.env.textresponses[key]);
  5097. 5097 : this.display_message('responseinserted', 'confirmation');
  5098. 5098 : };
  5099. 5099 :
  5100. 5100 : /**
  5101. 5101 : * Open the dialog to save a new canned response
  5102. 5102 : */
  5103. 5103 : this.save_response = function()
  5104. 5104 : {
  5105. 5105 : // show dialog to enter a name and to modify the text to be saved
  5106. 5106 : var buttons = {}, text = this.editor.get_content({selection: true, format: 'text', nosig: true}),
  5107. 5107 : html = '<form class="propform">' +
  5108. 5108 : '<div class="prop block"><label for="ffresponsename">' + this.get_label('responsename') + '</label>' +
  5109. 5109 : '<input type="text" name="name" id="ffresponsename" size="40" /></div>' +
  5110. 5110 : '<div class="prop block"><label for="ffresponsetext">' + this.get_label('responsetext') + '</label>' +
  5111. 5111 : '<textarea name="text" id="ffresponsetext" cols="40" rows="8"></textarea></div>' +
  5112. 5112 : '</form>';
  5113. 5113 :
  5114. 5114 : buttons[this.get_label('save')] = function(e) {
  5115. 5115 : var name = $('#ffresponsename').val(),
  5116. 5116 : text = $('#ffresponsetext').val();
  5117. 5117 :
  5118. 5118 : if (!text) {
  5119. 5119 : $('#ffresponsetext').select();
  5120. 5120 : return false;
  5121. 5121 : }
  5122. 5122 : if (!name)
  5123. 5123 : name = text.replace(/[\r\n]+/g, ' ').substring(0,40);
  5124. 5124 :
  5125. 5125 : var lock = ref.display_message('savingresponse', 'loading');
  5126. 5126 : ref.http_post('settings/responses', { _insert:1, _name:name, _text:text }, lock);
  5127. 5127 : $(this).dialog('close');
  5128. 5128 : };
  5129. 5129 :
  5130. 5130 : buttons[this.get_label('cancel')] = function() {
  5131. 5131 : $(this).dialog('close');
  5132. 5132 : };
  5133. 5133 :
  5134. 5134 : this.show_popup_dialog(html, this.get_label('newresponse'), buttons, {button_classes: ['mainaction save', 'cancel']});
  5135. 5135 :
  5136. 5136 : $('#ffresponsetext').val(text);
  5137. 5137 : $('#ffresponsename').select();
  5138. 5138 : };
  5139. 5139 :
  5140. 5140 : this.add_response_item = function(response)
  5141. 5141 : {
  5142. 5142 : var key = response.key;
  5143. 5143 : this.env.textresponses[key] = response;
  5144. 5144 :
  5145. 5145 : // append to responses list
  5146. 5146 : if (this.gui_objects.responseslist) {
  5147. 5147 : var li = $('<li>').appendTo(this.gui_objects.responseslist);
  5148. 5148 : $('<a>').addClass('insertresponse active')
  5149. 5149 : .attr({href: '#', rel: key, tabindex: '0'})
  5150. 5150 : .html(this.quote_html(response.name))
  5151. 5151 : .appendTo(li)
  5152. 5152 : .mousedown(function(e) {
  5153. 5153 : return rcube_event.cancel(e);
  5154. 5154 : })
  5155. 5155 : .on('mouseup keypress', function(e) {
  5156. 5156 : if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) {
  5157. 5157 : ref.command('insert-response', $(this).attr('rel'));
  5158. 5158 : $(document.body).trigger('mouseup'); // hides the menu
  5159. 5159 : return rcube_event.cancel(e);
  5160. 5160 : }
  5161. 5161 : });
  5162. 5162 :
  5163. 5163 : // remove the placeholder item if its there
  5164. 5164 : $(this.gui_objects.responseslist).find('li > a.insertresponse.placeholder').parent().remove();
  5165. 5165 : }
  5166. 5166 : };
  5167. 5167 :
  5168. 5168 : this.edit_responses = function()
  5169. 5169 : {
  5170. 5170 : // TODO: implement inline editing of responses
  5171. 5171 : };
  5172. 5172 :
  5173. 5173 : this.delete_response = function(key)
  5174. 5174 : {
  5175. 5175 : if (!key && this.responses_list) {
  5176. 5176 : var selection = this.responses_list.get_selection();
  5177. 5177 : key = selection[0];
  5178. 5178 : }
  5179. 5179 :
  5180. 5180 : // submit delete request
  5181. 5181 : if (key) {
  5182. 5182 : this.confirm_dialog(this.get_label('deleteresponseconfirm'), 'delete', function() {
  5183. 5183 : ref.http_post('settings/delete-response', { _key: key }, false);
  5184. 5184 : });
  5185. 5185 : }
  5186. 5186 : };
  5187. 5187 :
  5188. 5188 : // updates spellchecker buttons on state change
  5189. 5189 : this.spellcheck_state = function()
  5190. 5190 : {
  5191. 5191 : var active = this.editor.spellcheck_state();
  5192. 5192 :
  5193. 5193 : $.each(this.buttons.spellcheck || [], function(i, v) {
  5194. 5194 : $('#' + v.id)[active ? 'addClass' : 'removeClass']('selected');
  5195. 5195 : });
  5196. 5196 :
  5197. 5197 : return active;
  5198. 5198 : };
  5199. 5199 :
  5200. 5200 : // get selected language
  5201. 5201 : this.spellcheck_lang = function()
  5202. 5202 : {
  5203. 5203 : return this.editor.get_language();
  5204. 5204 : };
  5205. 5205 :
  5206. 5206 : this.spellcheck_lang_set = function(lang)
  5207. 5207 : {
  5208. 5208 : this.editor.set_language(lang);
  5209. 5209 : };
  5210. 5210 :
  5211. 5211 : // resume spellchecking, highlight provided misspellings without new ajax request
  5212. 5212 : this.spellcheck_resume = function(data)
  5213. 5213 : {
  5214. 5214 : this.editor.spellcheck_resume(data);
  5215. 5215 : };
  5216. 5216 :
  5217. 5217 : this.set_draft_id = function(id)
  5218. 5218 : {
  5219. 5219 : if (id && id != this.env.draft_id) {
  5220. 5220 : var filter = {task: 'mail', action: ''},
  5221. 5221 : rc = this.opener(false, filter) || this.opener(true, filter);
  5222. 5222 :
  5223. 5223 : // refresh the drafts folder in the opener window
  5224. 5224 : if (rc && rc.env.mailbox == this.env.drafts_mailbox)
  5225. 5225 : rc.command('checkmail');
  5226. 5226 :
  5227. 5227 : this.env.draft_id = id;
  5228. 5228 : $("[name='_draft_saveid']").val(id);
  5229. 5229 : }
  5230. 5230 :
  5231. 5231 : // always remove local copy upon saving as draft
  5232. 5232 : this.remove_compose_data(this.env.compose_id);
  5233. 5233 : this.compose_skip_unsavedcheck = false;
  5234. 5234 : };
  5235. 5235 :
  5236. 5236 : // Create (attach) 'savetarget' iframe before use
  5237. 5237 : this.get_save_target = function(unlock)
  5238. 5238 : {
  5239. 5239 : // Removing the frame on load/error to workaround issues with window history
  5240. 5240 : this.dummy_iframe('savetarget', 'about:blank')
  5241. 5241 : .on('load error', function() {
  5242. 5242 : // catch invalid/error response from server and unlock the UI (#7494, #7488, #7522)
  5243. 5243 : if (unlock && $(this).contents().find('meta[name="generator"][content="Roundcube"]').length == 0) {
  5244. 5244 : ref.iframe_loaded(unlock);
  5245. 5245 : ref.display_message('connerror', 'error');
  5246. 5246 : }
  5247. 5247 :
  5248. 5248 : $(this).remove();
  5249. 5249 : });
  5250. 5250 :
  5251. 5251 : return 'savetarget';
  5252. 5252 : };
  5253. 5253 :
  5254. 5254 : this.auto_save_start = function()
  5255. 5255 : {
  5256. 5256 : if (this.env.draft_autosave) {
  5257. 5257 : this.save_timer = setTimeout(function() {
  5258. 5258 : ref.command("savedraft");
  5259. 5259 : }, this.env.draft_autosave * 1000);
  5260. 5260 : }
  5261. 5261 :
  5262. 5262 : // save compose form content to local storage every 5 seconds
  5263. 5263 : if (!this.local_save_timer && window.localStorage && this.env.save_localstorage) {
  5264. 5264 : // track typing activity and only save on changes
  5265. 5265 : this.compose_type_activity = this.compose_type_activity_last = 0;
  5266. 5266 : $(document).keypress(function(e) { ref.compose_type_activity++; });
  5267. 5267 :
  5268. 5268 : this.local_save_timer = setInterval(function(){
  5269. 5269 : if (ref.compose_type_activity > ref.compose_type_activity_last) {
  5270. 5270 : ref.save_compose_form_local();
  5271. 5271 : ref.compose_type_activity_last = ref.compose_type_activity;
  5272. 5272 : }
  5273. 5273 : }, 5000);
  5274. 5274 :
  5275. 5275 : $(window).on('unload', function() {
  5276. 5276 : // remove copy from local storage if compose screen is left after warning
  5277. 5277 : if (!ref.env.server_error)
  5278. 5278 : ref.remove_compose_data(ref.env.compose_id);
  5279. 5279 : });
  5280. 5280 : }
  5281. 5281 :
  5282. 5282 : // check for unsaved changes before leaving the compose page
  5283. 5283 : if (!window.onbeforeunload) {
  5284. 5284 : window.onbeforeunload = function() {
  5285. 5285 : if (!ref.compose_skip_unsavedcheck && ref.cmp_hash != ref.compose_field_hash()) {
  5286. 5286 : return ref.get_label('notsentwarning');
  5287. 5287 : }
  5288. 5288 : };
  5289. 5289 : }
  5290. 5290 :
  5291. 5291 : // Unlock interface now that saving is complete
  5292. 5292 : this.busy = false;
  5293. 5293 : };
  5294. 5294 :
  5295. 5295 : this.compose_field_hash = function(save)
  5296. 5296 : {
  5297. 5297 : // check input fields
  5298. 5298 : var i, id, val, str = '', hash_fields = ['to', 'cc', 'bcc', 'subject'];
  5299. 5299 :
  5300. 5300 : for (i=0; i<hash_fields.length; i++)
  5301. 5301 : if (val = $('[name="_' + hash_fields[i] + '"]').val())
  5302. 5302 : str += val + ':';
  5303. 5303 :
  5304. 5304 : str += this.editor.get_content({refresh: false});
  5305. 5305 :
  5306. 5306 : for (id in this.env.attachments)
  5307. 5307 : str += id;
  5308. 5308 :
  5309. 5309 : // we can't detect changes in the Mailvelope editor so assume it changed
  5310. 5310 : if (this.mailvelope_editor) {
  5311. 5311 : str += ';' + new Date().getTime();
  5312. 5312 : }
  5313. 5313 :
  5314. 5314 : if (save)
  5315. 5315 : this.cmp_hash = str;
  5316. 5316 :
  5317. 5317 : return str;
  5318. 5318 : };
  5319. 5319 :
  5320. 5320 : // store the contents of the compose form to localstorage
  5321. 5321 : this.save_compose_form_local = function()
  5322. 5322 : {
  5323. 5323 : // feature is disabled
  5324. 5324 : if (!this.env.save_localstorage)
  5325. 5325 : return;
  5326. 5326 :
  5327. 5327 : var formdata = { session:this.env.session_id, changed:new Date().getTime() },
  5328. 5328 : ed, empty = true;
  5329. 5329 :
  5330. 5330 : // get fresh content from editor
  5331. 5331 : this.editor.save();
  5332. 5332 :
  5333. 5333 : if (this.env.draft_id) {
  5334. 5334 : formdata.draft_id = this.env.draft_id;
  5335. 5335 : }
  5336. 5336 : if (this.env.reply_msgid) {
  5337. 5337 : formdata.reply_msgid = this.env.reply_msgid;
  5338. 5338 : }
  5339. 5339 :
  5340. 5340 : $('input, select, textarea', this.gui_objects.messageform).each(function(i, elem) {
  5341. 5341 : switch (elem.tagName.toLowerCase()) {
  5342. 5342 : case 'input':
  5343. 5343 : if (elem.type == 'button' || elem.type == 'submit' || (elem.type == 'hidden' && elem.name != '_is_html')) {
  5344. 5344 : break;
  5345. 5345 : }
  5346. 5346 : formdata[elem.name] = elem.type != 'checkbox' || elem.checked ? $(elem).val() : '';
  5347. 5347 :
  5348. 5348 : if (formdata[elem.name] != '' && elem.type != 'hidden')
  5349. 5349 : empty = false;
  5350. 5350 : break;
  5351. 5351 :
  5352. 5352 : case 'select':
  5353. 5353 : formdata[elem.name] = $('option:checked', elem).val();
  5354. 5354 : break;
  5355. 5355 :
  5356. 5356 : default:
  5357. 5357 : formdata[elem.name] = $(elem).val();
  5358. 5358 : if (formdata[elem.name] != '')
  5359. 5359 : empty = false;
  5360. 5360 : }
  5361. 5361 : });
  5362. 5362 :
  5363. 5363 : if (!empty) {
  5364. 5364 : var index = this.local_storage_get_item('compose.index', []),
  5365. 5365 : key = this.env.compose_id;
  5366. 5366 :
  5367. 5367 : if ($.inArray(key, index) < 0) {
  5368. 5368 : index.push(key);
  5369. 5369 : }
  5370. 5370 :
  5371. 5371 : this.local_storage_set_item('compose.' + key, formdata, true);
  5372. 5372 : this.local_storage_set_item('compose.index', index);
  5373. 5373 : }
  5374. 5374 : };
  5375. 5375 :
  5376. 5376 : // write stored compose data back to form
  5377. 5377 : this.restore_compose_form = function(key, html_mode)
  5378. 5378 : {
  5379. 5379 : var ed, formdata = this.local_storage_get_item('compose.' + key, true);
  5380. 5380 :
  5381. 5381 : if (formdata && typeof formdata == 'object') {
  5382. 5382 : $.each(formdata, function(k, value) {
  5383. 5383 : if (k[0] == '_') {
  5384. 5384 : var elem = $("[name=" + k + "]");
  5385. 5385 : if (elem[0] && elem[0].type == 'checkbox') {
  5386. 5386 : elem.prop('checked', value != '');
  5387. 5387 : }
  5388. 5388 : else {
  5389. 5389 : elem.val(value).change();
  5390. 5390 : }
  5391. 5391 : }
  5392. 5392 : });
  5393. 5393 :
  5394. 5394 : // initialize HTML editor
  5395. 5395 : if ((formdata._is_html == '1' && !html_mode) || (formdata._is_html != '1' && html_mode)) {
  5396. 5396 : this.command('toggle-editor', {id: this.env.composebody, html: !html_mode, noconvert: true});
  5397. 5397 : }
  5398. 5398 : }
  5399. 5399 : };
  5400. 5400 :
  5401. 5401 : // remove stored compose data from localStorage
  5402. 5402 : this.remove_compose_data = function(key)
  5403. 5403 : {
  5404. 5404 : var index = this.local_storage_get_item('compose.index', []);
  5405. 5405 :
  5406. 5406 : if ($.inArray(key, index) >= 0) {
  5407. 5407 : this.local_storage_remove_item('compose.' + key);
  5408. 5408 : this.local_storage_set_item('compose.index', $.grep(index, function(val,i) { return val != key; }));
  5409. 5409 : }
  5410. 5410 : };
  5411. 5411 :
  5412. 5412 : // clear all stored compose data of this user
  5413. 5413 : this.clear_compose_data = function()
  5414. 5414 : {
  5415. 5415 : var i, index = this.local_storage_get_item('compose.index', []);
  5416. 5416 :
  5417. 5417 : for (i=0; i < index.length; i++) {
  5418. 5418 : this.local_storage_remove_item('compose.' + index[i]);
  5419. 5419 : }
  5420. 5420 :
  5421. 5421 : this.local_storage_remove_item('compose.index');
  5422. 5422 : };
  5423. 5423 :
  5424. 5424 : this.change_identity = function(obj, show)
  5425. 5425 : {
  5426. 5426 : if (!obj || !obj.options)
  5427. 5427 : return false;
  5428. 5428 :
  5429. 5429 : var id = $(obj).val(),
  5430. 5430 : got_sig = this.env.signatures && this.env.signatures[id],
  5431. 5431 : sig = this.env.identity,
  5432. 5432 : show_sig = show ? show : this.env.show_sig;
  5433. 5433 :
  5434. 5434 : // enable manual signature insert
  5435. 5435 : if (got_sig) {
  5436. 5436 : this.enable_command('insert-sig', true);
  5437. 5437 : this.env.compose_commands.push('insert-sig');
  5438. 5438 : got_sig = true;
  5439. 5439 : }
  5440. 5440 : else
  5441. 5441 : this.enable_command('insert-sig', false);
  5442. 5442 :
  5443. 5443 : // first function execution
  5444. 5444 : if (!this.env.identities_initialized) {
  5445. 5445 : this.env.identities_initialized = true;
  5446. 5446 : if (this.env.show_sig_later)
  5447. 5447 : this.env.show_sig = true;
  5448. 5448 : if (this.env.opened_extwin)
  5449. 5449 : return;
  5450. 5450 : }
  5451. 5451 :
  5452. 5452 : // update reply-to/bcc fields with addresses defined in identities
  5453. 5453 : $.each(['replyto', 'bcc'], function() {
  5454. 5454 : var rx, key = this,
  5455. 5455 : old_val = sig && ref.env.identities[sig] ? ref.env.identities[sig][key] : '',
  5456. 5456 : new_val = id && ref.env.identities[id] ? ref.env.identities[id][key] : '',
  5457. 5457 : input = $('[name="_'+key+'"]'), input_val = input.val();
  5458. 5458 :
  5459. 5459 : // remove old address(es)
  5460. 5460 : if (old_val && input_val) {
  5461. 5461 : rx = new RegExp('\\s*' + RegExp.escape(old_val) + '\\s*');
  5462. 5462 : input_val = input_val.replace(rx, '');
  5463. 5463 : }
  5464. 5464 :
  5465. 5465 : // cleanup
  5466. 5466 : input_val = String(input_val).replace(/[,;]\s*[,;]/g, ',').replace(/^[\s,;]+/, '');
  5467. 5467 :
  5468. 5468 : // add new address(es)
  5469. 5469 : if (new_val && input_val.indexOf(new_val) == -1 && input_val.indexOf(new_val.replace(/"/g, '')) == -1) {
  5470. 5470 : if (input_val) {
  5471. 5471 : input_val = input_val.replace(/[,;\s]+$/, '') + ', ';
  5472. 5472 : }
  5473. 5473 :
  5474. 5474 : input_val += new_val + ', ';
  5475. 5475 : }
  5476. 5476 :
  5477. 5477 : if (old_val || new_val)
  5478. 5478 : input.val(input_val).change();
  5479. 5479 : });
  5480. 5480 :
  5481. 5481 : if (this.editor)
  5482. 5482 : this.editor.change_signature(id, show_sig);
  5483. 5483 :
  5484. 5484 : if (show && got_sig)
  5485. 5485 : this.display_message('siginserted', 'confirmation');
  5486. 5486 :
  5487. 5487 : this.env.identity = id;
  5488. 5488 : this.triggerEvent('change_identity');
  5489. 5489 :
  5490. 5490 : return true;
  5491. 5491 : };
  5492. 5492 :
  5493. 5493 : // Open file selection dialog for defined upload form
  5494. 5494 : // Works only on click and only with smart-upload forms
  5495. 5495 : this.upload_input = function(name)
  5496. 5496 : {
  5497. 5497 : $('#' + name + ' input[type="file"]').click();
  5498. 5498 : };
  5499. 5499 :
  5500. 5500 : // upload (attachment) file
  5501. 5501 : this.upload_file = function(form, action, lock)
  5502. 5502 : {
  5503. 5503 : if (form) {
  5504. 5504 : var fname, files = [];
  5505. 5505 : $('input', form).each(function() {
  5506. 5506 : if (this.files) {
  5507. 5507 : fname = this.name;
  5508. 5508 : for (var i=0; i < this.files.length; i++)
  5509. 5509 : files.push(this.files[i]);
  5510. 5510 : }
  5511. 5511 : });
  5512. 5512 :
  5513. 5513 : return this.file_upload(files, {_id: this.env.compose_id || ''}, {
  5514. 5514 : name: fname,
  5515. 5515 : action: action,
  5516. 5516 : lock: lock
  5517. 5517 : });
  5518. 5518 : }
  5519. 5519 : };
  5520. 5520 :
  5521. 5521 : // add file name to attachment list
  5522. 5522 : // called from upload page
  5523. 5523 : this.add2attachment_list = function(name, att, upload_id)
  5524. 5524 : {
  5525. 5525 : if (upload_id)
  5526. 5526 : this.triggerEvent('fileuploaded', {name: name, attachment: att, id: upload_id});
  5527. 5527 :
  5528. 5528 : if (upload_id && this.env.attachments[upload_id])
  5529. 5529 : delete this.env.attachments[upload_id];
  5530. 5530 :
  5531. 5531 : this.env.attachments[name] = att;
  5532. 5532 :
  5533. 5533 : if (!this.gui_objects.attachmentlist)
  5534. 5534 : return false;
  5535. 5535 :
  5536. 5536 : var label, indicator, li = $('<li>');
  5537. 5537 :
  5538. 5538 : if (!att.complete && att.html.indexOf('<') < 0)
  5539. 5539 : att.html = '<span class="uploading">' + att.html + '</span>';
  5540. 5540 :
  5541. 5541 : if (!att.complete && this.env.loadingicon)
  5542. 5542 : att.html = '<img src="'+this.env.loadingicon+'" alt="" class="uploading" />' + att.html;
  5543. 5543 :
  5544. 5544 : if (!att.complete) {
  5545. 5545 : label = this.get_label('cancel');
  5546. 5546 : att.html = '<a title="'+label+'" onclick="return rcmail.cancel_attachment_upload(\''+name+'\');" href="#cancelupload" class="cancelupload">'
  5547. 5547 : + (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="'+label+'" />' : '<span class="inner">' + label + '</span>') + '</a>' + att.html;
  5548. 5548 : }
  5549. 5549 :
  5550. 5550 : li.attr('id', name).addClass(att.classname).html(att.html)
  5551. 5551 : .find('.attachment-name').on('mouseover', function() { rcube_webmail.long_subject_title_ex(this); });
  5552. 5552 :
  5553. 5553 : // replace indicator's li
  5554. 5554 : if (upload_id && (indicator = document.getElementById(upload_id))) {
  5555. 5555 : li.replaceAll(indicator);
  5556. 5556 : }
  5557. 5557 : else { // add new li
  5558. 5558 : li.appendTo(this.gui_objects.attachmentlist);
  5559. 5559 : }
  5560. 5560 :
  5561. 5561 : // set tabindex attribute
  5562. 5562 : var tabindex = $(this.gui_objects.attachmentlist).attr('data-tabindex') || '0';
  5563. 5563 : li.find('a').attr('tabindex', tabindex);
  5564. 5564 :
  5565. 5565 : this.triggerEvent('fileappended', {name: name, attachment: att, id: upload_id, item: li});
  5566. 5566 :
  5567. 5567 : return true;
  5568. 5568 : };
  5569. 5569 :
  5570. 5570 : this.remove_from_attachment_list = function(name)
  5571. 5571 : {
  5572. 5572 : delete this.env.attachments[name];
  5573. 5573 : $('#'+name).remove();
  5574. 5574 : };
  5575. 5575 :
  5576. 5576 : this.remove_attachment = function(name)
  5577. 5577 : {
  5578. 5578 : if (name && this.env.attachments[name])
  5579. 5579 : this.http_post('remove-attachment', { _id:this.env.compose_id, _file:name });
  5580. 5580 :
  5581. 5581 : return false;
  5582. 5582 : };
  5583. 5583 :
  5584. 5584 : this.cancel_attachment_upload = function(name)
  5585. 5585 : {
  5586. 5586 : if (!name || !this.uploads[name])
  5587. 5587 : return false;
  5588. 5588 :
  5589. 5589 : this.remove_from_attachment_list(name);
  5590. 5590 : this.uploads[name].abort();
  5591. 5591 : return false;
  5592. 5592 : };
  5593. 5593 :
  5594. 5594 : // rename uploaded attachment (in compose)
  5595. 5595 : this.rename_attachment = function(id)
  5596. 5596 : {
  5597. 5597 : var attachment = this.env.attachments[id];
  5598. 5598 :
  5599. 5599 : if (!attachment)
  5600. 5600 : return;
  5601. 5601 :
  5602. 5602 : var input = $('<input>').attr({type: 'text', size: 50}).val(attachment.name),
  5603. 5603 : content = $('<label>').text(this.get_label('namex')).append(input);
  5604. 5604 :
  5605. 5605 : this.simple_dialog(content, 'attachmentrename', function() {
  5606. 5606 : var name;
  5607. 5607 : if ((name = input.val()) && name != attachment.name) {
  5608. 5608 : ref.http_post('rename-attachment', {_id: ref.env.compose_id, _file: id, _name: name},
  5609. 5609 : ref.set_busy(true, 'loading'));
  5610. 5610 : return true;
  5611. 5611 : }
  5612. 5612 : }
  5613. 5613 : );
  5614. 5614 : };
  5615. 5615 :
  5616. 5616 : // update attachments list with the new name
  5617. 5617 : this.rename_attachment_handler = function(id, name)
  5618. 5618 : {
  5619. 5619 : var attachment = this.env.attachments[id];
  5620. 5620 :
  5621. 5621 : if (!attachment || !name)
  5622. 5622 : return;
  5623. 5623 :
  5624. 5624 : attachment.name = name;
  5625. 5625 :
  5626. 5626 : $('#' + id + ' .attachment-name').text(name).attr('title', '');
  5627. 5627 : };
  5628. 5628 :
  5629. 5629 : // send remote request to add a new contact
  5630. 5630 : this.add_contact = function(value, reload, source)
  5631. 5631 : {
  5632. 5632 : if (value)
  5633. 5633 : this.http_post('addcontact', {_address: value, _reload: reload, _source: source});
  5634. 5634 : };
  5635. 5635 :
  5636. 5636 : // send remote request to search mail or contacts
  5637. 5637 : this.qsearch = function(value)
  5638. 5638 : {
  5639. 5639 : // Note: Some plugins would like to do search without value,
  5640. 5640 : // so we keep value != '' check to allow that use-case. Which means
  5641. 5641 : // e.g. that qsearch() with no argument will execute the search.
  5642. 5642 : if (value != '' || $(this.gui_objects.qsearchbox).val() || $(this.gui_objects.search_interval).val()) {
  5643. 5643 : var r, lock = this.set_busy(true, 'searching'),
  5644. 5644 : url = this.search_params(value),
  5645. 5645 : action = this.env.action == 'compose' && this.contact_list ? 'search-contacts' : 'search';
  5646. 5646 :
  5647. 5647 : if (this.message_list)
  5648. 5648 : this.clear_message_list();
  5649. 5649 : else if (this.contact_list)
  5650. 5650 : this.list_contacts_clear();
  5651. 5651 :
  5652. 5652 : if (this.env.source)
  5653. 5653 : url._source = this.env.source;
  5654. 5654 : if (this.env.group)
  5655. 5655 : url._gid = this.env.group;
  5656. 5656 :
  5657. 5657 : // reset vars
  5658. 5658 : this.env.current_page = 1;
  5659. 5659 :
  5660. 5660 : r = this.http_request(action, url, lock);
  5661. 5661 :
  5662. 5662 : this.env.qsearch = {lock: lock, request: r};
  5663. 5663 : this.enable_command('set-listmode', this.env.threads && (this.env.search_scope || 'base') == 'base');
  5664. 5664 :
  5665. 5665 : return true;
  5666. 5666 : }
  5667. 5667 :
  5668. 5668 : return false;
  5669. 5669 : };
  5670. 5670 :
  5671. 5671 : this.continue_search = function(request_id)
  5672. 5672 : {
  5673. 5673 : var lock = this.set_busy(true, 'stillsearching');
  5674. 5674 :
  5675. 5675 : setTimeout(function() {
  5676. 5676 : var url = ref.search_params();
  5677. 5677 : url._continue = request_id;
  5678. 5678 : ref.env.qsearch = { lock: lock, request: ref.http_request('search', url, lock) };
  5679. 5679 : }, 100);
  5680. 5680 : };
  5681. 5681 :
  5682. 5682 : // build URL params for search
  5683. 5683 : this.search_params = function(search, filter)
  5684. 5684 : {
  5685. 5685 : var n, url = {}, mods_arr = [],
  5686. 5686 : mods = this.env.search_mods,
  5687. 5687 : scope = this.env.search_scope || 'base',
  5688. 5688 : mbox = this.env.mailbox;
  5689. 5689 :
  5690. 5690 : if (!filter && this.gui_objects.search_filter)
  5691. 5691 : filter = this.gui_objects.search_filter.value;
  5692. 5692 :
  5693. 5693 : if (!search && this.gui_objects.qsearchbox)
  5694. 5694 : search = this.gui_objects.qsearchbox.value;
  5695. 5695 :
  5696. 5696 : if (this.gui_objects.search_interval)
  5697. 5697 : url._interval = $(this.gui_objects.search_interval).val();
  5698. 5698 :
  5699. 5699 : if (search) {
  5700. 5700 : url._q = search;
  5701. 5701 :
  5702. 5702 : if (mods && this.message_list)
  5703. 5703 : mods = mods[mbox] || mods['*'];
  5704. 5704 :
  5705. 5705 : if (mods) {
  5706. 5706 : for (n in mods)
  5707. 5707 : mods_arr.push(n);
  5708. 5708 : url._headers = mods_arr.join(',');
  5709. 5709 : }
  5710. 5710 : }
  5711. 5711 :
  5712. 5712 : url._layout = this.env.layout;
  5713. 5713 : url._filter = filter;
  5714. 5714 : url._scope = scope;
  5715. 5715 : url._mbox = mbox;
  5716. 5716 :
  5717. 5717 : return url;
  5718. 5718 : };
  5719. 5719 :
  5720. 5720 : // reset search filter
  5721. 5721 : this.reset_search_filter = function()
  5722. 5722 : {
  5723. 5723 : this.filter_disabled = true;
  5724. 5724 : if (this.gui_objects.search_filter)
  5725. 5725 : $(this.gui_objects.search_filter).val('ALL').change();
  5726. 5726 : this.filter_disabled = false;
  5727. 5727 : };
  5728. 5728 :
  5729. 5729 : // reset quick-search form
  5730. 5730 : this.reset_qsearch = function(all)
  5731. 5731 : {
  5732. 5732 : if (this.gui_objects.qsearchbox)
  5733. 5733 : this.gui_objects.qsearchbox.value = '';
  5734. 5734 :
  5735. 5735 : if (this.gui_objects.search_interval)
  5736. 5736 : $(this.gui_objects.search_interval).val('');
  5737. 5737 :
  5738. 5738 : if (this.env.qsearch)
  5739. 5739 : this.abort_request(this.env.qsearch);
  5740. 5740 :
  5741. 5741 : if (all) {
  5742. 5742 : this.env.search_scope = 'base';
  5743. 5743 : this.reset_search_filter();
  5744. 5744 : }
  5745. 5745 :
  5746. 5746 : this.env.qsearch = null;
  5747. 5747 : this.env.search_request = null;
  5748. 5748 : this.env.search_id = null;
  5749. 5749 : this.select_all_mode = false;
  5750. 5750 :
  5751. 5751 : this.enable_command('set-listmode', this.env.threads);
  5752. 5752 : };
  5753. 5753 :
  5754. 5754 : this.set_searchscope = function(scope)
  5755. 5755 : {
  5756. 5756 : this.env.search_scope = scope;
  5757. 5757 : };
  5758. 5758 :
  5759. 5759 : this.set_searchinterval = function(interval)
  5760. 5760 : {
  5761. 5761 : this.env.search_interval = interval;
  5762. 5762 : };
  5763. 5763 :
  5764. 5764 : this.set_searchmods = function(mods)
  5765. 5765 : {
  5766. 5766 : var mbox = this.env.mailbox,
  5767. 5767 : scope = this.env.search_scope || 'base';
  5768. 5768 :
  5769. 5769 : if (!this.env.search_mods)
  5770. 5770 : this.env.search_mods = {};
  5771. 5771 :
  5772. 5772 : if (mbox)
  5773. 5773 : this.env.search_mods[mbox] = mods;
  5774. 5774 :
  5775. 5775 : // MANTIS 0005913: Problème pour une recherche dans tous les dossiers
  5776. 5776 : if (mbox != '*')
  5777. 5777 : this.env.search_mods['*'] = mods;
  5778. 5778 : };
  5779. 5779 :
  5780. 5780 : this.is_multifolder_listing = function()
  5781. 5781 : {
  5782. 5782 : return this.env.multifolder_listing !== undefined ? this.env.multifolder_listing :
  5783. 5783 : (this.env.search_request && (this.env.search_scope || 'base') != 'base');
  5784. 5784 : };
  5785. 5785 :
  5786. 5786 : // action executed after mail is sent
  5787. 5787 : this.sent_successfully = function(type, msg, folders, save_error)
  5788. 5788 : {
  5789. 5789 : this.display_message(msg, type);
  5790. 5790 : this.compose_skip_unsavedcheck = true;
  5791. 5791 :
  5792. 5792 : if (this.env.extwin) {
  5793. 5793 : if (!save_error)
  5794. 5794 : this.lock_form(this.gui_objects.messageform);
  5795. 5795 :
  5796. 5796 : var filter = {task: 'mail', action: ''},
  5797. 5797 : rc = this.opener(false, filter) || this.opener(true, filter);
  5798. 5798 :
  5799. 5799 : if (rc) {
  5800. 5800 : rc.display_message(msg, type);
  5801. 5801 : // refresh the folder where sent message was saved or replied message comes from
  5802. 5802 : if (folders && $.inArray(rc.env.mailbox, folders) >= 0) {
  5803. 5803 : rc.command('checkmail');
  5804. 5804 : }
  5805. 5805 : }
  5806. 5806 :
  5807. 5807 : if (!save_error)
  5808. 5808 : setTimeout(function() { window.close(); }, 1000);
  5809. 5809 : }
  5810. 5810 : else if (!save_error) {
  5811. 5811 : // before redirect we need to wait some time for Chrome (#1486177)
  5812. 5812 : setTimeout(function() { ref.list_mailbox(); }, 500);
  5813. 5813 : }
  5814. 5814 :
  5815. 5815 : if (save_error)
  5816. 5816 : this.env.is_sent = true;
  5817. 5817 : };
  5818. 5818 :
  5819. 5819 : this.image_rotate = function()
  5820. 5820 : {
  5821. 5821 : var curr = this.image_style ? (this.image_style.rotate || 0) : 0;
  5822. 5822 :
  5823. 5823 : this.image_style.rotate = curr > 180 ? 0 : curr + 90;
  5824. 5824 : this.apply_image_style();
  5825. 5825 : };
  5826. 5826 :
  5827. 5827 : this.image_scale = function(prop)
  5828. 5828 : {
  5829. 5829 : var curr = this.image_style ? (this.image_style.scale || 1) : 1;
  5830. 5830 :
  5831. 5831 : this.image_style.scale = Math.max(0.1, curr + 0.1 * (prop == '-' ? -1 : 1));
  5832. 5832 : this.apply_image_style();
  5833. 5833 : };
  5834. 5834 :
  5835. 5835 : this.apply_image_style = function()
  5836. 5836 : {
  5837. 5837 : var style = [],
  5838. 5838 : head = $(this.gui_objects.messagepartframe).contents().find('head');
  5839. 5839 :
  5840. 5840 : $('#image-style', head).remove();
  5841. 5841 :
  5842. 5842 : $.each({scale: '', rotate: 'deg'}, function(i, v) {
  5843. 5843 : var val = ref.image_style[i];
  5844. 5844 : if (val)
  5845. 5845 : style.push(i + '(' + val + v + ')');
  5846. 5846 : });
  5847. 5847 :
  5848. 5848 : if (style)
  5849. 5849 : head.append($('<style id="image-style">').text('img { transform: ' + style.join(' ') + '}'));
  5850. 5850 : };
  5851. 5851 :
  5852. 5852 : // Update import dialog state
  5853. 5853 : this.import_state_set = function(state)
  5854. 5854 : {
  5855. 5855 : if (this.import_dialog) {
  5856. 5856 : this.import_state = state;
  5857. 5857 :
  5858. 5858 : var button = $(this.import_dialog).parent().find('.ui-dialog-buttonset > button').first();
  5859. 5859 :
  5860. 5860 : if (state != 'error') {
  5861. 5861 : // replace Import/Cancel buttons with Close button
  5862. 5862 : button.hide();
  5863. 5863 : button.next().text(this.gettext('close')).focus();
  5864. 5864 : }
  5865. 5865 : else {
  5866. 5866 : // activate the Import button
  5867. 5867 : button.prop('disabled', false);
  5868. 5868 : }
  5869. 5869 : }
  5870. 5870 : };
  5871. 5871 :
  5872. 5872 :
  5873. 5873 : /*********************************************************/
  5874. 5874 : /********* keyboard live-search methods *********/
  5875. 5875 : /*********************************************************/
  5876. 5876 :
  5877. 5877 : // handler for keyboard events on address-fields
  5878. 5878 : this.ksearch_keydown = function(e, obj, props)
  5879. 5879 : {
  5880. 5880 : if (this.ksearch_timer)
  5881. 5881 : clearTimeout(this.ksearch_timer);
  5882. 5882 :
  5883. 5883 : var key = rcube_event.get_keycode(e);
  5884. 5884 :
  5885. 5885 : switch (key) {
  5886. 5886 : case 38: // arrow up
  5887. 5887 : case 40: // arrow down
  5888. 5888 : if (!this.ksearch_visible())
  5889. 5889 : return;
  5890. 5890 :
  5891. 5891 : var dir = key == 38 ? 1 : 0,
  5892. 5892 : highlight = this.ksearch_pane.find('li.selected')[0];
  5893. 5893 :
  5894. 5894 : if (!highlight)
  5895. 5895 : highlight = this.ksearch_pane.__ul.firstChild;
  5896. 5896 :
  5897. 5897 : if (highlight)
  5898. 5898 : this.ksearch_select(dir ? highlight.previousSibling : highlight.nextSibling);
  5899. 5899 :
  5900. 5900 : return rcube_event.cancel(e);
  5901. 5901 :
  5902. 5902 : case 9: // tab
  5903. 5903 : if (rcube_event.get_modifier(e) == SHIFT_KEY || !this.ksearch_visible()) {
  5904. 5904 : this.ksearch_hide();
  5905. 5905 : return;
  5906. 5906 : }
  5907. 5907 :
  5908. 5908 : case 13: // enter
  5909. 5909 : if (!this.ksearch_visible())
  5910. 5910 : return false;
  5911. 5911 :
  5912. 5912 : // insert selected address and hide ksearch pane
  5913. 5913 : this.insert_recipient(this.ksearch_selected);
  5914. 5914 : this.ksearch_hide();
  5915. 5915 :
  5916. 5916 : // Don't cancel on Tab, we want to jump to the next field (#5659)
  5917. 5917 : return key == 9 ? null : rcube_event.cancel(e);
  5918. 5918 :
  5919. 5919 : case 27: // escape
  5920. 5920 : this.ksearch_hide();
  5921. 5921 : return;
  5922. 5922 :
  5923. 5923 : case 37: // left
  5924. 5924 : case 39: // right
  5925. 5925 : return;
  5926. 5926 : }
  5927. 5927 :
  5928. 5928 : // start timer
  5929. 5929 : this.ksearch_timer = setTimeout(function() { ref.ksearch_get_results(props); }, 200);
  5930. 5930 : this.ksearch_input = obj;
  5931. 5931 :
  5932. 5932 : return true;
  5933. 5933 : };
  5934. 5934 :
  5935. 5935 : this.ksearch_visible = function()
  5936. 5936 : {
  5937. 5937 : return this.ksearch_selected !== null && this.ksearch_selected !== undefined && this.ksearch_value;
  5938. 5938 : };
  5939. 5939 :
  5940. 5940 : this.ksearch_select = function(node)
  5941. 5941 : {
  5942. 5942 : if (this.ksearch_pane && node) {
  5943. 5943 : this.ksearch_pane.find('li.selected').removeClass('selected').removeAttr('aria-selected');
  5944. 5944 : }
  5945. 5945 :
  5946. 5946 : if (node) {
  5947. 5947 : $(node).addClass('selected').attr('aria-selected', 'true');
  5948. 5948 : this.ksearch_selected = node._rcm_id;
  5949. 5949 : $(this.ksearch_input).attr('aria-activedescendant', 'rcmkSearchItem' + this.ksearch_selected);
  5950. 5950 : }
  5951. 5951 : };
  5952. 5952 :
  5953. 5953 : this.insert_recipient = function(id)
  5954. 5954 : {
  5955. 5955 : if (id === null || !this.env.contacts[id] || !this.ksearch_input)
  5956. 5956 : return;
  5957. 5957 :
  5958. 5958 : var trigger = false, insert = '', delim = ', ',
  5959. 5959 : contact = this.env.contacts[id];
  5960. 5960 :
  5961. 5961 : this.ksearch_destroy();
  5962. 5962 :
  5963. 5963 : // insert all members of a group
  5964. 5964 : if (typeof contact === 'object' && contact.type == 'group' && !contact.email && contact.id) {
  5965. 5965 : // We wrap the group name with invisible markers to prevent from problems with group expanding (#7569)
  5966. 5966 : var name = '\u200b' + contact.name + '\u200b';
  5967. 5967 : insert = name + delim;
  5968. 5968 : this.group2expand[contact.id] = {name: name, input: this.ksearch_input};
  5969. 5969 : this.http_request('mail/group-expand', {_source: contact.source, _gid: contact.id}, false);
  5970. 5970 : }
  5971. 5971 : else if (typeof contact === 'object' && contact.name) {
  5972. 5972 : insert = contact.name + delim;
  5973. 5973 : trigger = true;
  5974. 5974 : }
  5975. 5975 : else if (typeof contact === 'string') {
  5976. 5976 : insert = contact + delim;
  5977. 5977 : trigger = true;
  5978. 5978 : }
  5979. 5979 :
  5980. 5980 : this.ksearch_input_replace(this.ksearch_value, insert, null, trigger);
  5981. 5981 :
  5982. 5982 : if (trigger) {
  5983. 5983 : this.triggerEvent('autocomplete_insert', {
  5984. 5984 : field: this.ksearch_input,
  5985. 5985 : insert: insert,
  5986. 5986 : data: contact,
  5987. 5987 : search: this.ksearch_value_last,
  5988. 5988 : result_type: 'person'
  5989. 5989 : });
  5990. 5990 :
  5991. 5991 : this.ksearch_value_last = null;
  5992. 5992 : this.compose_type_activity++;
  5993. 5993 : }
  5994. 5994 : };
  5995. 5995 :
  5996. 5996 : this.replace_group_recipients = function(id, recipients)
  5997. 5997 : {
  5998. 5998 : var data = this.group2expand[id];
  5999. 5999 :
  6000. 6000 : if (data) {
  6001. 6001 : this.ksearch_input_replace(data.name, recipients, data.input);
  6002. 6002 :
  6003. 6003 : this.triggerEvent('autocomplete_insert', {
  6004. 6004 : field: data.input,
  6005. 6005 : insert: recipients,
  6006. 6006 : data: data,
  6007. 6007 : search: this.ksearch_value_last,
  6008. 6008 : result_type: 'group'
  6009. 6009 : });
  6010. 6010 :
  6011. 6011 : this.ksearch_value_last = null;
  6012. 6012 : this.group2expand[id] = null;
  6013. 6013 : this.compose_type_activity++;
  6014. 6014 : }
  6015. 6015 : };
  6016. 6016 :
  6017. 6017 : // address search processor
  6018. 6018 : this.ksearch_get_results = function(props)
  6019. 6019 : {
  6020. 6020 : if (this.ksearch_pane && this.ksearch_pane.is(":visible"))
  6021. 6021 : this.ksearch_pane.hide();
  6022. 6022 :
  6023. 6023 : // get string from cursor position back to the last comma or semicolon
  6024. 6024 : var q = this.ksearch_input_get(),
  6025. 6025 : min = this.env.autocomplete_min_length,
  6026. 6026 : data = this.ksearch_data;
  6027. 6027 :
  6028. 6028 : // trim query string
  6029. 6029 : q = q.trim();
  6030. 6030 :
  6031. 6031 : // Don't (re-)search if the last results are still active
  6032. 6032 : if (q == this.ksearch_value)
  6033. 6033 : return;
  6034. 6034 :
  6035. 6035 : this.ksearch_destroy();
  6036. 6036 :
  6037. 6037 : if (q.length && q.length < min) {
  6038. 6038 : if (!this.ksearch_info) {
  6039. 6039 : this.ksearch_info = this.display_message(this.get_label('autocompletechars').replace('$min', min));
  6040. 6040 : }
  6041. 6041 : return;
  6042. 6042 : }
  6043. 6043 :
  6044. 6044 : var old_value = this.ksearch_value;
  6045. 6045 : this.ksearch_value = q;
  6046. 6046 : this.ksearch_value_last = q; // Group expansion clears ksearch_value before calling autocomplete_insert trigger, therefore store it in separate variable for later consumption.
  6047. 6047 :
  6048. 6048 : // ...string is empty
  6049. 6049 : if (!q.length)
  6050. 6050 : return;
  6051. 6051 :
  6052. 6052 : // ...new search value contains old one and previous search was not finished or its result was empty
  6053. 6053 : if (old_value && old_value.length && q.startsWith(old_value) && (!data || data.num <= 0) && this.env.contacts && !this.env.contacts.length)
  6054. 6054 : return;
  6055. 6055 :
  6056. 6056 : var sources = props && props.sources ? props.sources : [''];
  6057. 6057 : var reqid = this.multi_thread_http_request({
  6058. 6058 : items: sources,
  6059. 6059 : threads: props && props.threads ? props.threads : 1,
  6060. 6060 : action: props && props.action ? props.action : 'mail/autocomplete',
  6061. 6061 : postdata: { _search:q, _source:'%s' },
  6062. 6062 : lock: this.display_message('searching', 'loading')
  6063. 6063 : });
  6064. 6064 :
  6065. 6065 : this.ksearch_data = { id:reqid, sources:sources.slice(), num:sources.length };
  6066. 6066 : };
  6067. 6067 :
  6068. 6068 : this.ksearch_query_results = function(results, search, reqid)
  6069. 6069 : {
  6070. 6070 : // trigger multi-thread http response callback
  6071. 6071 : this.multi_thread_http_response(results, reqid);
  6072. 6072 :
  6073. 6073 : // search stopped in meantime?
  6074. 6074 : if (!this.ksearch_value)
  6075. 6075 : return;
  6076. 6076 :
  6077. 6077 : // ignore this outdated search response
  6078. 6078 : if (this.ksearch_input && search != this.ksearch_value)
  6079. 6079 : return;
  6080. 6080 :
  6081. 6081 : // display search results
  6082. 6082 : var i, id, len, ul, text, type, init,
  6083. 6083 : is_framed = this.is_framed(),
  6084. 6084 : value = this.ksearch_value,
  6085. 6085 : maxlen = this.env.autocomplete_max ? this.env.autocomplete_max : 15;
  6086. 6086 :
  6087. 6087 : // create results pane if not present
  6088. 6088 : if (!this.ksearch_pane) {
  6089. 6089 : ul = $('<ul>');
  6090. 6090 : this.ksearch_pane = $('<div>')
  6091. 6091 : .attr({id: 'rcmKSearchpane', role: 'listbox', 'class': 'select-menu inline'})
  6092. 6092 : .css({position: 'absolute', 'z-index': 30000})
  6093. 6093 : .append(ul)
  6094. 6094 : .appendTo(is_framed ? parent.document.body : document.body);
  6095. 6095 :
  6096. 6096 : this.ksearch_pane.__ul = ul[0];
  6097. 6097 : this.triggerEvent('autocomplete_create', {obj: this.ksearch_pane});
  6098. 6098 : }
  6099. 6099 :
  6100. 6100 : ul = this.ksearch_pane.__ul;
  6101. 6101 :
  6102. 6102 : // remove all search results or add to existing list if parallel search
  6103. 6103 : if (reqid && this.ksearch_pane.data('reqid') == reqid) {
  6104. 6104 : maxlen -= ul.childNodes.length;
  6105. 6105 : }
  6106. 6106 : else {
  6107. 6107 : this.ksearch_pane.data('reqid', reqid);
  6108. 6108 : init = 1;
  6109. 6109 : // reset content
  6110. 6110 : ul.innerHTML = '';
  6111. 6111 : this.env.contacts = [];
  6112. 6112 :
  6113. 6113 : // Calculate the results pane position and size
  6114. 6114 : // Elastic: On small screen we use the width/position of the whole .ac-input element (input's parent)
  6115. 6115 : var is_composite_input = $('html').is('.layout-small,.layout-phone') && $(this.ksearch_input).parents('.ac-input').length == 1,
  6116. 6116 : input = is_composite_input ? $(this.ksearch_input).parents('.ac-input')[0] : $(this.ksearch_input)[0],
  6117. 6117 : pos = $(input).offset();
  6118. 6118 :
  6119. 6119 : // ... consider scroll position
  6120. 6120 : pos.left -= $(document.documentElement).scrollLeft();
  6121. 6121 : pos.top -= $(document.documentElement).scrollTop();
  6122. 6122 :
  6123. 6123 : // ... consider iframe position
  6124. 6124 : if (is_framed) {
  6125. 6125 : try {
  6126. 6126 : parent.$('iframe').each(function() {
  6127. 6127 : if (this.contentWindow == window) {
  6128. 6128 : var offset = $(this).offset();
  6129. 6129 : pos.left += offset.left;
  6130. 6130 : pos.top += offset.top;
  6131. 6131 : }
  6132. 6132 : });
  6133. 6133 : }
  6134. 6134 : catch(e) {}
  6135. 6135 : }
  6136. 6136 :
  6137. 6137 : var w = $(is_framed ? parent : window).width(),
  6138. 6138 : input_width = $(input).outerWidth(),
  6139. 6139 : left = w - pos.left > 200 ? pos.left : w - 200,
  6140. 6140 : top = (pos.top + input.offsetHeight + 1),
  6141. 6141 : //PAMELA - Changement de la taille de l'input pour les grands écrans
  6142. 6142 : width = $(input).outerWidth() >= 400 ? $(input).outerWidth() : Math.min(400, w - left);
  6143. 6143 :
  6144. 6144 : this.ksearch_pane.css({
  6145. 6145 : left: (is_composite_input ? pos.left : left) + 'px',
  6146. 6146 : top: top + 'px',
  6147. 6147 : maxWidth: (is_composite_input ? input_width : width) + 'px',
  6148. 6148 : minWidth: '200px',
  6149. 6149 : width: is_composite_input ? (input_width + 'px') : 'auto',
  6150. 6150 : display: 'none'
  6151. 6151 : });
  6152. 6152 : }
  6153. 6153 :
  6154. 6154 : // add each result line to list
  6155. 6155 : if (results && (len = results.length)) {
  6156. 6156 : for (i=0; i < len && maxlen > 0; i++) {
  6157. 6157 : text = typeof results[i] === 'object' ? (results[i].display || results[i].name) : results[i];
  6158. 6158 : type = typeof results[i] === 'object' ? results[i].type : '';
  6159. 6159 : id = i + this.env.contacts.length;
  6160. 6160 : if (this.env.contacts.length && this.env.autocomplete_clean_duplicates) {
  6161. 6161 : if (this.env.contacts.find(contact => contact.name == text || contact.display == text)) {
  6162. 6162 : continue;
  6163. 6163 : }
  6164. 6164 : }
  6165. 6165 : $('<li>').attr({id: 'rcmkSearchItem' + id, role: 'option'})
  6166. 6166 : .html('<i class="icon"></i>' + this.quote_html(text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '<b>$1</b>'))
  6167. 6167 : .addClass(type || '')
  6168. 6168 : .appendTo(ul)
  6169. 6169 : .mouseover(function() { ref.ksearch_select(this); })
  6170. 6170 : .mouseup(function() { ref.ksearch_click(this); })
  6171. 6171 : .get(0)._rcm_id = id;
  6172. 6172 : maxlen -= 1;
  6173. 6173 : }
  6174. 6174 : }
  6175. 6175 :
  6176. 6176 : if (ul.childNodes.length) {
  6177. 6177 : // set the right aria-* attributes to the input field
  6178. 6178 : $(this.ksearch_input)
  6179. 6179 : .attr({'aria-haspopup': 'true', 'aria-expanded': 'true', 'aria-owns': 'rcmKSearchpane'});
  6180. 6180 :
  6181. 6181 : this.ksearch_pane.show();
  6182. 6182 :
  6183. 6183 : // select the first
  6184. 6184 : if (!this.env.contacts.length) {
  6185. 6185 : this.ksearch_select($('li', ul)[0]);
  6186. 6186 : }
  6187. 6187 : }
  6188. 6188 :
  6189. 6189 : if (len)
  6190. 6190 : this.env.contacts = this.env.contacts.concat(results);
  6191. 6191 :
  6192. 6192 : if (this.ksearch_data.id == reqid)
  6193. 6193 : this.ksearch_data.num--;
  6194. 6194 : };
  6195. 6195 :
  6196. 6196 : // Getter for input value
  6197. 6197 : // returns a string from the last comma to current cursor position
  6198. 6198 : this.ksearch_input_get = function()
  6199. 6199 : {
  6200. 6200 : if (!this.ksearch_input)
  6201. 6201 : return '';
  6202. 6202 :
  6203. 6203 : var cp = this.get_caret_pos(this.ksearch_input);
  6204. 6204 :
  6205. 6205 : return this.ksearch_input.value.substr(0, cp).split(/[,;]/).pop();
  6206. 6206 : };
  6207. 6207 :
  6208. 6208 : // Setter for input value
  6209. 6209 : // replaces 'from' string with 'to' and sets cursor position at the end
  6210. 6210 : this.ksearch_input_replace = function(from, to, input, trigger)
  6211. 6211 : {
  6212. 6212 : if (!this.ksearch_input && !input)
  6213. 6213 : return;
  6214. 6214 :
  6215. 6215 : if (!input)
  6216. 6216 : input = this.ksearch_input;
  6217. 6217 :
  6218. 6218 : var cpos = this.get_caret_pos(input),
  6219. 6219 : p = input.value.lastIndexOf(from, cpos),
  6220. 6220 : pre = input.value.substring(0, p),
  6221. 6221 : end = input.value.substring(p + from.length, input.value.length);
  6222. 6222 :
  6223. 6223 : input.value = pre + to + end;
  6224. 6224 :
  6225. 6225 : // set caret to insert pos
  6226. 6226 : this.set_caret_pos(input, cpos + to.length - from.length);
  6227. 6227 :
  6228. 6228 : // run onchange action on the element
  6229. 6229 : $(input).trigger('change', [true, trigger]);
  6230. 6230 : };
  6231. 6231 :
  6232. 6232 : this.ksearch_click = function(node)
  6233. 6233 : {
  6234. 6234 : if (this.ksearch_input)
  6235. 6235 : this.ksearch_input.focus();
  6236. 6236 :
  6237. 6237 : this.insert_recipient(node._rcm_id);
  6238. 6238 : this.ksearch_hide();
  6239. 6239 : };
  6240. 6240 :
  6241. 6241 : this.ksearch_blur = function()
  6242. 6242 : {
  6243. 6243 : if (this.ksearch_timer)
  6244. 6244 : clearTimeout(this.ksearch_timer);
  6245. 6245 :
  6246. 6246 : this.ksearch_input = null;
  6247. 6247 : this.ksearch_hide();
  6248. 6248 : };
  6249. 6249 :
  6250. 6250 : this.ksearch_hide = function()
  6251. 6251 : {
  6252. 6252 : this.ksearch_selected = null;
  6253. 6253 : this.ksearch_value = '';
  6254. 6254 :
  6255. 6255 : if (this.ksearch_pane)
  6256. 6256 : this.ksearch_pane.hide();
  6257. 6257 :
  6258. 6258 : $(this.ksearch_input)
  6259. 6259 : .attr({'aria-haspopup': 'false', 'aria-expanded': 'false'})
  6260. 6260 : .removeAttr('aria-activedescendant')
  6261. 6261 : .removeAttr('aria-owns');
  6262. 6262 :
  6263. 6263 : this.ksearch_destroy();
  6264. 6264 : };
  6265. 6265 :
  6266. 6266 : // Clears autocomplete data/requests
  6267. 6267 : this.ksearch_destroy = function()
  6268. 6268 : {
  6269. 6269 : if (this.ksearch_data)
  6270. 6270 : this.multi_thread_request_abort(this.ksearch_data.id);
  6271. 6271 :
  6272. 6272 : if (this.ksearch_info)
  6273. 6273 : this.hide_message(this.ksearch_info);
  6274. 6274 :
  6275. 6275 : if (this.ksearch_msg)
  6276. 6276 : this.hide_message(this.ksearch_msg);
  6277. 6277 :
  6278. 6278 : this.ksearch_data = null;
  6279. 6279 : this.ksearch_info = null;
  6280. 6280 : this.ksearch_msg = null;
  6281. 6281 : };
  6282. 6282 :
  6283. 6283 :
  6284. 6284 : /*********************************************************/
  6285. 6285 : /********* address book methods *********/
  6286. 6286 : /*********************************************************/
  6287. 6287 :
  6288. 6288 : this.contactlist_select = function(list)
  6289. 6289 : {
  6290. 6290 : if (this.preview_timer)
  6291. 6291 : clearTimeout(this.preview_timer);
  6292. 6292 :
  6293. 6293 : var id, targets,
  6294. 6294 : groupcount = 0,
  6295. 6295 : writable = false,
  6296. 6296 : deletable = false,
  6297. 6297 : copy_writable = false,
  6298. 6298 : selected = list.get_selection().length,
  6299. 6299 : source = this.env.source ? this.env.address_sources[this.env.source] : null;
  6300. 6300 :
  6301. 6301 : // we don't have dblclick handler here, so use 50 instead of this.dblclick_time
  6302. 6302 : if (this.env.contentframe && !list.multi_selecting && (id = list.get_single_selection()))
  6303. 6303 : this.preview_timer = setTimeout(function() { ref.load_contact(id, 'show'); }, this.preview_delay_click);
  6304. 6304 : else if (this.env.contentframe)
  6305. 6305 : this.show_contentframe(false);
  6306. 6306 :
  6307. 6307 : if (selected) {
  6308. 6308 : list.draggable = false;
  6309. 6309 :
  6310. 6310 : // no source = search result, we'll need to detect if any of
  6311. 6311 : // selected contacts are in writable addressbook to enable edit/delete
  6312. 6312 : // we'll also need to know sources used in selection for copy
  6313. 6313 : // and group-addmember operations (drag&drop)
  6314. 6314 : this.env.selection_sources = [];
  6315. 6315 :
  6316. 6316 : if (source) {
  6317. 6317 : this.env.selection_sources.push(this.env.source);
  6318. 6318 : }
  6319. 6319 :
  6320. 6320 : $.each(list.get_selection(), function(i, v) {
  6321. 6321 : var book, sid, contact = list.data[v];
  6322. 6322 : if (!source) {
  6323. 6323 : sid = String(v).replace(/^[^-]+-/, '');
  6324. 6324 : book = sid ? ref.env.address_sources[sid] : null;
  6325. 6325 :
  6326. 6326 : if (book) {
  6327. 6327 : writable = writable || (!book.readonly && !contact.readonly);
  6328. 6328 : deletable = deletable || book.deletable === true;
  6329. 6329 : ref.env.selection_sources.push(sid);
  6330. 6330 : }
  6331. 6331 : }
  6332. 6332 : else {
  6333. 6333 : writable = writable || (!source.readonly && !contact.readonly);
  6334. 6334 : deletable = deletable || source.deletable === true;
  6335. 6335 : }
  6336. 6336 :
  6337. 6337 : if (contact._type != 'group')
  6338. 6338 : list.draggable = true;
  6339. 6339 : });
  6340. 6340 :
  6341. 6341 : this.env.selection_sources = $.unique(this.env.selection_sources);
  6342. 6342 :
  6343. 6343 : if (source && source.groups)
  6344. 6344 : $.each(this.env.contactgroups, function() { if (this.source === ref.env.source) groupcount++; });
  6345. 6345 :
  6346. 6346 : targets = $.map(this.env.address_sources, function(v, i) { return v.readonly ? null : i; });
  6347. 6347 : copy_writable = $.grep(targets, function(v) { return jQuery.inArray(v, ref.env.selection_sources) < 0; }).length > 0;
  6348. 6348 : }
  6349. 6349 :
  6350. 6350 : // if a group is currently selected, and there is at least one contact selected
  6351. 6351 : // we can enable the group-remove-selected command
  6352. 6352 : this.enable_command('group-assign-selected', groupcount > 0 && writable);
  6353. 6353 : this.enable_command('group-remove-selected', this.env.group && writable);
  6354. 6354 : this.enable_command('print', 'qrcode', selected == 1);
  6355. 6355 : this.enable_command('export-selected', selected > 0);
  6356. 6356 : this.enable_command('edit', id && writable);
  6357. 6357 : this.enable_command('delete', 'move', writable || deletable);
  6358. 6358 : this.enable_command('copy', copy_writable);
  6359. 6359 :
  6360. 6360 : return false;
  6361. 6361 : };
  6362. 6362 :
  6363. 6363 : this.list_contacts = function(src, group, page, search)
  6364. 6364 : {
  6365. 6365 : var win, folder, index = -1, url = {},
  6366. 6366 : refresh = src === undefined && group === undefined && page === undefined,
  6367. 6367 : target = window;
  6368. 6368 :
  6369. 6369 : if (!src)
  6370. 6370 : src = this.env.source;
  6371. 6371 :
  6372. 6372 : if (refresh)
  6373. 6373 : group = this.env.group;
  6374. 6374 :
  6375. 6375 : if (src != this.env.source) {
  6376. 6376 : page = this.env.current_page = 1;
  6377. 6377 : this.reset_qsearch();
  6378. 6378 : }
  6379. 6379 : else if (!refresh && group != this.env.group)
  6380. 6380 : page = this.env.current_page = 1;
  6381. 6381 :
  6382. 6382 : if (this.env.search_id)
  6383. 6383 : folder = 'S'+this.env.search_id;
  6384. 6384 : else if (!this.env.search_request)
  6385. 6385 : folder = group ? 'G'+src+group : src;
  6386. 6386 :
  6387. 6387 : this.env.source = this.env.last_source = src;
  6388. 6388 : this.env.group = this.env.last_group = group;
  6389. 6389 :
  6390. 6390 : // truncate groups listing stack
  6391. 6391 : $.each(this.env.address_group_stack, function(i, v) {
  6392. 6392 : if (ref.env.group == v.id) {
  6393. 6393 : index = i;
  6394. 6394 : return false;
  6395. 6395 : }
  6396. 6396 : });
  6397. 6397 :
  6398. 6398 : this.env.address_group_stack = index < 0 ? [] : this.env.address_group_stack.slice(0, index);
  6399. 6399 :
  6400. 6400 : // remove cached contact group selector
  6401. 6401 : this.destroy_entity_selector('contactgroup-selector');
  6402. 6402 :
  6403. 6403 : // make sure the current group is on top of the stack
  6404. 6404 : if (this.env.group) {
  6405. 6405 : if (!search) search = {};
  6406. 6406 : search.id = this.env.group;
  6407. 6407 : this.env.address_group_stack.push(search);
  6408. 6408 :
  6409. 6409 : // mark the first group on the stack as selected in the directory list
  6410. 6410 : folder = 'G'+src+this.env.address_group_stack[0].id;
  6411. 6411 : }
  6412. 6412 : else if (this.gui_objects.addresslist_title) {
  6413. 6413 : $(this.gui_objects.addresslist_title).text(this.get_label('contacts'));
  6414. 6414 : }
  6415. 6415 :
  6416. 6416 : if (!this.env.search_id)
  6417. 6417 : this.select_folder(folder, '', true);
  6418. 6418 :
  6419. 6419 : // load contacts remotely
  6420. 6420 : if (this.gui_objects.contactslist) {
  6421. 6421 : this.list_contacts_remote(src, group, page);
  6422. 6422 : return;
  6423. 6423 : }
  6424. 6424 :
  6425. 6425 : if (win = this.get_frame_window(this.env.contentframe)) {
  6426. 6426 : target = win;
  6427. 6427 : url._framed = 1;
  6428. 6428 : }
  6429. 6429 :
  6430. 6430 : if (group)
  6431. 6431 : url._gid = group;
  6432. 6432 : if (page)
  6433. 6433 : url._page = page;
  6434. 6434 : if (src)
  6435. 6435 : url._source = src;
  6436. 6436 :
  6437. 6437 : // also send search request to get the correct listing
  6438. 6438 : if (this.env.search_request)
  6439. 6439 : url._search = this.env.search_request;
  6440. 6440 :
  6441. 6441 : this.set_busy(true, 'loading');
  6442. 6442 : this.location_href(url, target);
  6443. 6443 : };
  6444. 6444 :
  6445. 6445 : // send remote request to load contacts list
  6446. 6446 : this.list_contacts_remote = function(src, group, page)
  6447. 6447 : {
  6448. 6448 : // clear message list first
  6449. 6449 : this.list_contacts_clear();
  6450. 6450 :
  6451. 6451 : // send request to server
  6452. 6452 : var url = {}, lock = this.set_busy(true, 'loading');
  6453. 6453 :
  6454. 6454 : if (src)
  6455. 6455 : url._source = src;
  6456. 6456 : if (page)
  6457. 6457 : url._page = page;
  6458. 6458 : if (group)
  6459. 6459 : url._gid = group;
  6460. 6460 :
  6461. 6461 : this.env.source = src;
  6462. 6462 : this.env.group = group;
  6463. 6463 :
  6464. 6464 : // also send search request to get the right records
  6465. 6465 : if (this.env.search_request)
  6466. 6466 : url._search = this.env.search_request;
  6467. 6467 :
  6468. 6468 : this.http_request(this.env.task == 'mail' ? 'list-contacts' : 'list', url, lock);
  6469. 6469 :
  6470. 6470 : if (this.env.task != 'mail')
  6471. 6471 : this.update_state({_source: src, _page: page && page > 1 ? page : null, _gid: group});
  6472. 6472 : };
  6473. 6473 :
  6474. 6474 : this.list_contacts_clear = function()
  6475. 6475 : {
  6476. 6476 : this.contact_list.data = {};
  6477. 6477 : this.contact_list.clear(true);
  6478. 6478 : this.show_contentframe(false);
  6479. 6479 : this.enable_command('delete', 'move', 'copy', 'print', false);
  6480. 6480 : };
  6481. 6481 :
  6482. 6482 : this.set_group_prop = function(prop)
  6483. 6483 : {
  6484. 6484 : if (this.gui_objects.addresslist_title) {
  6485. 6485 : var boxtitle = $(this.gui_objects.addresslist_title).html(''); // clear contents
  6486. 6486 :
  6487. 6487 : // add link to pop back to parent group
  6488. 6488 : if (this.env.address_group_stack.length > 1
  6489. 6489 : || (this.env.address_group_stack.length == 1 && this.env.address_group_stack[0].search_request)
  6490. 6490 : ) {
  6491. 6491 : var link = $('<a href="#list">...</a>')
  6492. 6492 : .attr({title: this.get_label('uponelevel'), 'class': 'poplink'})
  6493. 6493 : .click(function() { return ref.command('popgroup', '', this); });
  6494. 6494 :
  6495. 6495 : boxtitle.append(link).append('&nbsp;&raquo;&nbsp;');
  6496. 6496 : }
  6497. 6497 :
  6498. 6498 : boxtitle.append($('<span>').text(prop ? prop.name : this.get_label('contacts')));
  6499. 6499 : }
  6500. 6500 : };
  6501. 6501 :
  6502. 6502 : // load contact record
  6503. 6503 : this.load_contact = function(cid, action, framed)
  6504. 6504 : {
  6505. 6505 : var win, url = {}, target = window,
  6506. 6506 : rec = this.contact_list ? this.contact_list.data[cid] : null;
  6507. 6507 :
  6508. 6508 : if (win = this.get_frame_window(this.env.contentframe)) {
  6509. 6509 : url._framed = 1;
  6510. 6510 : target = win;
  6511. 6511 : this.show_contentframe(true);
  6512. 6512 :
  6513. 6513 : // load dummy content, unselect selected row(s)
  6514. 6514 : if (!cid)
  6515. 6515 : this.contact_list.clear_selection();
  6516. 6516 :
  6517. 6517 : this.enable_command('export-selected', 'print', rec && rec._type != 'group');
  6518. 6518 : }
  6519. 6519 : else if (framed)
  6520. 6520 : return false;
  6521. 6521 :
  6522. 6522 : if (action && (cid || action == 'add') && !this.drag_active) {
  6523. 6523 : if (this.env.group)
  6524. 6524 : url._gid = this.env.group;
  6525. 6525 :
  6526. 6526 : if (this.env.search_request)
  6527. 6527 : url._search = this.env.search_request;
  6528. 6528 :
  6529. 6529 : if (cid)
  6530. 6530 : url._cid = this.preview_id = cid;
  6531. 6531 :
  6532. 6532 : url._action = action;
  6533. 6533 : url._source = this.env.source;
  6534. 6534 :
  6535. 6535 : this.location_href(url, target, true);
  6536. 6536 : }
  6537. 6537 :
  6538. 6538 : return true;
  6539. 6539 : };
  6540. 6540 :
  6541. 6541 : // add/delete member to/from the group
  6542. 6542 : this.group_member_change = function(what, cid, source, gid)
  6543. 6543 : {
  6544. 6544 : if (what != 'add')
  6545. 6545 : what = 'del';
  6546. 6546 :
  6547. 6547 : var lock = this.display_message(what == 'add' ? 'addingmember' : 'removingmember', 'loading'),
  6548. 6548 : post_data = {_cid: cid, _source: source, _gid: gid};
  6549. 6549 :
  6550. 6550 : this.http_post('group-'+what+'members', post_data, lock);
  6551. 6551 : };
  6552. 6552 :
  6553. 6553 : this.contacts_drag_menu = function(e, to)
  6554. 6554 : {
  6555. 6555 : var dest = to.type == 'group' ? to.source : to.id,
  6556. 6556 : source = this.env.source;
  6557. 6557 :
  6558. 6558 : if (!this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
  6559. 6559 : return true;
  6560. 6560 :
  6561. 6561 : // search result may contain contacts from many sources, but if there is only one...
  6562. 6562 : if (source == '' && this.env.selection_sources.length == 1)
  6563. 6563 : source = this.env.selection_sources[0];
  6564. 6564 :
  6565. 6565 : if (to.type == 'group' && dest == source) {
  6566. 6566 : var cid = this.contact_list.get_selection().join(',');
  6567. 6567 : this.group_member_change('add', cid, dest, to.id);
  6568. 6568 : return true;
  6569. 6569 : }
  6570. 6570 : // move action is not possible, "redirect" to copy if menu wasn't requested
  6571. 6571 : else if (!this.commands.move && rcube_event.get_modifier(e) != SHIFT_KEY) {
  6572. 6572 : this.copy_contacts(to);
  6573. 6573 : return true;
  6574. 6574 : }
  6575. 6575 :
  6576. 6576 : return this.drag_menu(e, to);
  6577. 6577 : };
  6578. 6578 :
  6579. 6579 : // copy contact(s) to the specified target (group or directory)
  6580. 6580 : this.copy_contacts = function(to, event, cid)
  6581. 6581 : {
  6582. 6582 : if (!to) {
  6583. 6583 : cid = this.contact_list.get_selection();
  6584. 6584 : return this.addressbook_selector(event, function(to, obj) {
  6585. 6585 : var to = $(obj).data('source') ? ref.env.contactgroups['G' + $(obj).data('source') + $(obj).data('gid')] : ref.env.address_sources[to];
  6586. 6586 : ref.copy_contacts(to, null, cid);
  6587. 6587 : });
  6588. 6588 : }
  6589. 6589 :
  6590. 6590 : var dest = to.type == 'group' ? to.source : to.id,
  6591. 6591 : source = this.env.source,
  6592. 6592 : group = this.env.group ? this.env.group : '';
  6593. 6593 :
  6594. 6594 : cid = cid ? cid.join(',') : this.contact_list.get_selection().join(',');
  6595. 6595 :
  6596. 6596 : if (!cid || !this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
  6597. 6597 : return;
  6598. 6598 :
  6599. 6599 : // search result may contain contacts from many sources, but if there is only one...
  6600. 6600 : if (source == '' && this.env.selection_sources.length == 1)
  6601. 6601 : source = this.env.selection_sources[0];
  6602. 6602 :
  6603. 6603 : // target is a group
  6604. 6604 : if (to.type == 'group') {
  6605. 6605 : if (dest == source)
  6606. 6606 : return;
  6607. 6607 :
  6608. 6608 : var lock = this.display_message('copyingcontact', 'loading'),
  6609. 6609 : post_data = {_cid: cid, _source: this.env.source, _to: dest, _togid: to.id, _gid: group};
  6610. 6610 :
  6611. 6611 : this.http_post('copy', post_data, lock);
  6612. 6612 : }
  6613. 6613 : // target is an addressbook
  6614. 6614 : else if (to.id != source) {
  6615. 6615 : var lock = this.display_message('copyingcontact', 'loading'),
  6616. 6616 : post_data = {_cid: cid, _source: this.env.source, _to: to.id, _gid: group};
  6617. 6617 :
  6618. 6618 : this.http_post('copy', post_data, lock);
  6619. 6619 : }
  6620. 6620 : };
  6621. 6621 :
  6622. 6622 : // move contact(s) to the specified target (group or directory)
  6623. 6623 : this.move_contacts = function(to, event, cid)
  6624. 6624 : {
  6625. 6625 : if (!to) {
  6626. 6626 : cid = this.contact_list.get_selection();
  6627. 6627 : return this.addressbook_selector(event, function(to, obj) {
  6628. 6628 : var to = $(obj).data('source') ? ref.env.contactgroups['G' + $(obj).data('source') + $(obj).data('gid')] : ref.env.address_sources[to];
  6629. 6629 : ref.move_contacts(to, null, cid);
  6630. 6630 : });
  6631. 6631 : }
  6632. 6632 :
  6633. 6633 : var dest = to.type == 'group' ? to.source : to.id,
  6634. 6634 : source = this.env.source,
  6635. 6635 : group = this.env.group ? this.env.group : '';
  6636. 6636 :
  6637. 6637 : if (!this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
  6638. 6638 : return;
  6639. 6639 :
  6640. 6640 : if (!cid)
  6641. 6641 : cid = this.contact_list.get_selection();
  6642. 6642 :
  6643. 6643 : // search result may contain contacts from many sources, but if there is only one...
  6644. 6644 : if (source == '' && this.env.selection_sources.length == 1)
  6645. 6645 : source = this.env.selection_sources[0];
  6646. 6646 :
  6647. 6647 : if (to.type == 'group') {
  6648. 6648 : if (dest == source)
  6649. 6649 : return;
  6650. 6650 :
  6651. 6651 : this._with_selected_contacts('move', {_to: dest, _togid: to.id, _cid: cid});
  6652. 6652 : }
  6653. 6653 : // target is an addressbook
  6654. 6654 : else if (to.id != source)
  6655. 6655 : this._with_selected_contacts('move', {_to: to.id, _cid: cid});
  6656. 6656 : };
  6657. 6657 :
  6658. 6658 : // delete contact(s)
  6659. 6659 : this.delete_contacts = function()
  6660. 6660 : {
  6661. 6661 : var undelete = this.env.source && this.env.address_sources[this.env.source].undelete;
  6662. 6662 :
  6663. 6663 : if (undelete) {
  6664. 6664 : this._with_selected_contacts('delete', {_cid: this.contact_list.get_selection()});
  6665. 6665 : }
  6666. 6666 : else {
  6667. 6667 : var cid = this.contact_list.get_selection();
  6668. 6668 : this.confirm_dialog(this.get_label('deletecontactconfirm'), 'delete', function() {
  6669. 6669 : ref._with_selected_contacts('delete', {_cid: cid});
  6670. 6670 : });
  6671. 6671 : }
  6672. 6672 : };
  6673. 6673 :
  6674. 6674 : this._with_selected_contacts = function(action, post_data)
  6675. 6675 : {
  6676. 6676 : var selection = post_data._cid;
  6677. 6677 :
  6678. 6678 : // exit if no contact specified or if selection is empty
  6679. 6679 : if (!selection.length && !this.env.cid)
  6680. 6680 : return;
  6681. 6681 :
  6682. 6682 : var n, a_cids = [],
  6683. 6683 : label = action == 'delete' ? 'contactdeleting' : 'movingcontact',
  6684. 6684 : lock = this.display_message(label, 'loading'),
  6685. 6685 : display_next = this.check_display_next();
  6686. 6686 :
  6687. 6687 : if (this.env.cid)
  6688. 6688 : a_cids.push(this.env.cid);
  6689. 6689 : else {
  6690. 6690 : for (n=0; n<selection.length; n++) {
  6691. 6691 : id = selection[n];
  6692. 6692 : a_cids.push(id);
  6693. 6693 : this.contact_list.remove_row(id, display_next && n == selection.length-1);
  6694. 6694 : }
  6695. 6695 :
  6696. 6696 : if (!display_next)
  6697. 6697 : this.contact_list.clear_selection();
  6698. 6698 : }
  6699. 6699 :
  6700. 6700 : if (!post_data)
  6701. 6701 : post_data = {};
  6702. 6702 :
  6703. 6703 : post_data._source = this.env.source;
  6704. 6704 : post_data._from = this.env.action;
  6705. 6705 : post_data._cid = a_cids.join(',');
  6706. 6706 :
  6707. 6707 : if (this.env.group)
  6708. 6708 : post_data._gid = this.env.group;
  6709. 6709 :
  6710. 6710 : // also send search request to get the right records from the next page
  6711. 6711 : if (this.env.search_request)
  6712. 6712 : post_data._search = this.env.search_request;
  6713. 6713 :
  6714. 6714 : // send request to server
  6715. 6715 : this.http_post(action, post_data, lock)
  6716. 6716 :
  6717. 6717 : return true;
  6718. 6718 : };
  6719. 6719 :
  6720. 6720 : // update a contact record in the list
  6721. 6721 : this.update_contact_row = function(cid, cols_arr, newcid, source, data)
  6722. 6722 : {
  6723. 6723 : var list = this.contact_list;
  6724. 6724 :
  6725. 6725 : cid = this.html_identifier(cid);
  6726. 6726 :
  6727. 6727 : // when in searching mode, concat cid with the source name
  6728. 6728 : if (!list.rows[cid]) {
  6729. 6729 : cid = cid + '-' + source;
  6730. 6730 : if (newcid)
  6731. 6731 : newcid = newcid + '-' + source;
  6732. 6732 : }
  6733. 6733 :
  6734. 6734 : list.update_row(cid, cols_arr, newcid, true);
  6735. 6735 : list.data[cid] = data;
  6736. 6736 : };
  6737. 6737 :
  6738. 6738 : // add row to contacts list
  6739. 6739 : this.add_contact_row = function(cid, cols, classes, data)
  6740. 6740 : {
  6741. 6741 : if (!this.gui_objects.contactslist)
  6742. 6742 : return false;
  6743. 6743 :
  6744. 6744 : var c, col, list = this.contact_list,
  6745. 6745 : row = { cols:[] };
  6746. 6746 :
  6747. 6747 : row.id = 'rcmrow' + this.html_identifier(cid);
  6748. 6748 : row.className = 'contact ' + (classes || '');
  6749. 6749 :
  6750. 6750 : if (list.in_selection(cid))
  6751. 6751 : row.className += ' selected';
  6752. 6752 :
  6753. 6753 : // add each submitted col
  6754. 6754 : for (c in cols) {
  6755. 6755 : col = {};
  6756. 6756 : col.className = String(c).toLowerCase();
  6757. 6757 : col.innerHTML = cols[c];
  6758. 6758 : row.cols.push(col);
  6759. 6759 : }
  6760. 6760 :
  6761. 6761 : // store data in list member
  6762. 6762 : list.data[cid] = data;
  6763. 6763 : list.insert_row(row);
  6764. 6764 :
  6765. 6765 : this.enable_command('export', list.rowcount > 0);
  6766. 6766 : };
  6767. 6767 :
  6768. 6768 : this.init_contact_form = function()
  6769. 6769 : {
  6770. 6770 : var col;
  6771. 6771 :
  6772. 6772 : if (this.env.coltypes) {
  6773. 6773 : this.set_photo_actions($('#ff_photo').val());
  6774. 6774 : for (col in this.env.coltypes)
  6775. 6775 : this.init_edit_field(col, null);
  6776. 6776 : }
  6777. 6777 :
  6778. 6778 : $('.contactfieldgroup .row a.deletebutton').click(function() {
  6779. 6779 : ref.delete_edit_field(this);
  6780. 6780 : return false;
  6781. 6781 : });
  6782. 6782 :
  6783. 6783 : $('select.addfieldmenu').change(function() {
  6784. 6784 : ref.insert_edit_field($(this).val(), $(this).attr('rel'), this);
  6785. 6785 : this.selectedIndex = 0;
  6786. 6786 : });
  6787. 6787 :
  6788. 6788 : // enable date pickers on date fields
  6789. 6789 : if ($.datepicker && this.env.date_format) {
  6790. 6790 : $.datepicker.setDefaults({
  6791. 6791 : dateFormat: this.env.date_format,
  6792. 6792 : changeMonth: true,
  6793. 6793 : changeYear: true,
  6794. 6794 : yearRange: '-120:+10',
  6795. 6795 : showOtherMonths: true,
  6796. 6796 : selectOtherMonths: true
  6797. 6797 : });
  6798. 6798 : $('input.datepicker').datepicker();
  6799. 6799 : }
  6800. 6800 :
  6801. 6801 : // Submit search form on Enter
  6802. 6802 : if (this.env.action == 'search')
  6803. 6803 : $(this.gui_objects.editform).append($('<input type="submit">').hide())
  6804. 6804 : .submit(function() { $('input.mainaction').click(); return false; });
  6805. 6805 : };
  6806. 6806 :
  6807. 6807 : // group creation dialog
  6808. 6808 : this.group_create = function()
  6809. 6809 : {
  6810. 6810 : var input = $('<input>').attr({type: 'text', 'data-submit': 'true'}),
  6811. 6811 : content = $('<label>').text(this.get_label('namex')).append(input),
  6812. 6812 : source = this.env.source;
  6813. 6813 :
  6814. 6814 : this.simple_dialog(content, 'newgroup', function() {
  6815. 6815 : var name;
  6816. 6816 : if (name = input.val()) {
  6817. 6817 : ref.http_post('group-create', {_source: source, _name: name},
  6818. 6818 : ref.set_busy(true, 'loading'));
  6819. 6819 : return true;
  6820. 6820 : }
  6821. 6821 : });
  6822. 6822 : };
  6823. 6823 :
  6824. 6824 : // group rename dialog
  6825. 6825 : this.group_rename = function()
  6826. 6826 : {
  6827. 6827 : if (!this.env.group)
  6828. 6828 : return;
  6829. 6829 :
  6830. 6830 : var group_name = this.env.contactgroups['G' + this.env.source + this.env.group].name,
  6831. 6831 : input = $('<input>').attr({type: 'text', 'data-submit': 'true'}).val(group_name),
  6832. 6832 : content = $('<label>').text(this.get_label('namex')).append(input),
  6833. 6833 : source = this.env.source,
  6834. 6834 : group = this.env.group;
  6835. 6835 :
  6836. 6836 : this.simple_dialog(content, 'grouprename', function() {
  6837. 6837 : var name;
  6838. 6838 : if ((name = input.val()) && name != group_name) {
  6839. 6839 : ref.http_post('group-rename', {_source: source, _gid: group, _name: name},
  6840. 6840 : ref.set_busy(true, 'loading'));
  6841. 6841 : return true;
  6842. 6842 : }
  6843. 6843 : });
  6844. 6844 : };
  6845. 6845 :
  6846. 6846 : this.group_delete = function()
  6847. 6847 : {
  6848. 6848 : if (this.env.group) {
  6849. 6849 : var group = this.env.group;
  6850. 6850 : this.confirm_dialog(this.get_label('deletegroupconfirm'), 'delete', function() {
  6851. 6851 : var lock = ref.set_busy(true, 'groupdeleting');
  6852. 6852 : ref.http_post('group-delete', {_source: ref.env.source, _gid: group}, lock);
  6853. 6853 : });
  6854. 6854 : }
  6855. 6855 : };
  6856. 6856 :
  6857. 6857 : // callback from server upon group-delete command
  6858. 6858 : this.remove_group_item = function(prop)
  6859. 6859 : {
  6860. 6860 : var key = 'G' + prop.source + prop.id;
  6861. 6861 :
  6862. 6862 : if (this.treelist.remove(key)) {
  6863. 6863 : // make sure there is no cached address book or contact group selectors
  6864. 6864 : this.destroy_entity_selector('addressbook-selector');
  6865. 6865 : this.destroy_entity_selector('contactgroup-selector');
  6866. 6866 :
  6867. 6867 : this.triggerEvent('group_delete', { source:prop.source, id:prop.id });
  6868. 6868 : delete this.env.contactfolders[key];
  6869. 6869 : delete this.env.contactgroups[key];
  6870. 6870 : }
  6871. 6871 :
  6872. 6872 : if (prop.source == this.env.source && prop.id == this.env.group)
  6873. 6873 : this.list_contacts(prop.source, 0);
  6874. 6874 : };
  6875. 6875 :
  6876. 6876 : //assign selected contacts to a group
  6877. 6877 : this.group_assign_selected = function(props, obj, event)
  6878. 6878 : {
  6879. 6879 : var cid = ref.contact_list.get_selection();
  6880. 6880 : var source = ref.env.source;
  6881. 6881 : this.contactgroup_selector(event, function(to) { ref.group_member_change('add', cid, source, to); });
  6882. 6882 : };
  6883. 6883 :
  6884. 6884 : //remove selected contacts from current active group
  6885. 6885 : this.group_remove_selected = function()
  6886. 6886 : {
  6887. 6887 : this.http_post('group-delmembers', {_cid: this.contact_list.get_selection(),
  6888. 6888 : _source: this.env.source, _gid: this.env.group});
  6889. 6889 : };
  6890. 6890 :
  6891. 6891 : //callback after deleting contact(s) from current group
  6892. 6892 : this.remove_group_contacts = function(props)
  6893. 6893 : {
  6894. 6894 : if (this.env.group !== undefined && (this.env.group === props.gid)) {
  6895. 6895 : var n, selection = this.contact_list.get_selection(),
  6896. 6896 : display_next= this.check_display_next();
  6897. 6897 :
  6898. 6898 : for (n=0; n<selection.length; n++) {
  6899. 6899 : id = selection[n];
  6900. 6900 : this.contact_list.remove_row(id, display_next && n == selection.length-1);
  6901. 6901 : }
  6902. 6902 :
  6903. 6903 : if (!display_next)
  6904. 6904 : this.contact_list.clear_selection();
  6905. 6905 : }
  6906. 6906 : };
  6907. 6907 :
  6908. 6908 : // callback for creating a new contact group
  6909. 6909 : this.insert_contact_group = function(prop)
  6910. 6910 : {
  6911. 6911 : prop.type = 'group';
  6912. 6912 :
  6913. 6913 : var key = 'G'+prop.source+prop.id,
  6914. 6914 : link = $('<a>').attr({href: '#', rel: prop.source + ':' + prop.id})
  6915. 6915 : .click(function() { return ref.command('listgroup', prop, this); })
  6916. 6916 : .text(prop.name);
  6917. 6917 :
  6918. 6918 : this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
  6919. 6919 : this.treelist.insert({ id:key, html:link, classes:['contactgroup'] }, prop.source, 'contactgroup');
  6920. 6920 :
  6921. 6921 : // make sure there is no cached address book or contact group selectors
  6922. 6922 : this.destroy_entity_selector('addressbook-selector');
  6923. 6923 : this.destroy_entity_selector('contactgroup-selector');
  6924. 6924 :
  6925. 6925 : this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key) });
  6926. 6926 : };
  6927. 6927 :
  6928. 6928 : // callback for renaming a contact group
  6929. 6929 : this.update_contact_group = function(prop)
  6930. 6930 : {
  6931. 6931 : var key = 'G'+prop.source+prop.id,
  6932. 6932 : newnode = {};
  6933. 6933 :
  6934. 6934 : // group ID has changed, replace link node and identifiers
  6935. 6935 : if (prop.newid) {
  6936. 6936 : var newkey = 'G'+prop.source+prop.newid,
  6937. 6937 : newprop = $.extend({}, prop);
  6938. 6938 :
  6939. 6939 : this.env.contactfolders[newkey] = this.env.contactfolders[key];
  6940. 6940 : this.env.contactfolders[newkey].id = prop.newid;
  6941. 6941 : this.env.group = prop.newid;
  6942. 6942 :
  6943. 6943 : delete this.env.contactfolders[key];
  6944. 6944 : delete this.env.contactgroups[key];
  6945. 6945 :
  6946. 6946 : newprop.id = prop.newid;
  6947. 6947 : newprop.type = 'group';
  6948. 6948 :
  6949. 6949 : newnode.id = newkey;
  6950. 6950 : newnode.html = $('<a>').attr({href: '#', rel: prop.source + ':' + prop.newid})
  6951. 6951 : .click(function() { return ref.command('listgroup', newprop, this); })
  6952. 6952 : .text(prop.name);
  6953. 6953 : }
  6954. 6954 : // update displayed group name
  6955. 6955 : else {
  6956. 6956 : $(this.treelist.get_item(key)).children().first().text(prop.name);
  6957. 6957 : this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
  6958. 6958 :
  6959. 6959 : if (prop.source == this.env.source && prop.id == this.env.group)
  6960. 6960 : this.set_group_prop(prop);
  6961. 6961 : }
  6962. 6962 :
  6963. 6963 : // update list node and re-sort it
  6964. 6964 : this.treelist.update(key, newnode, true);
  6965. 6965 :
  6966. 6966 : // make sure there is no cached address book or contact group selectors
  6967. 6967 : this.destroy_entity_selector('addressbook-selector');
  6968. 6968 : this.destroy_entity_selector('contactgroup-selector');
  6969. 6969 :
  6970. 6970 : this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key), newid:prop.newid });
  6971. 6971 : };
  6972. 6972 :
  6973. 6973 : this.update_group_commands = function()
  6974. 6974 : {
  6975. 6975 : var source = this.env.source != '' ? this.env.address_sources[this.env.source] : null,
  6976. 6976 : supported = source && source.groups && !source.readonly;
  6977. 6977 :
  6978. 6978 : this.enable_command('group-create', supported);
  6979. 6979 : this.enable_command('group-rename', 'group-delete', supported && this.env.group);
  6980. 6980 : };
  6981. 6981 :
  6982. 6982 : this.init_edit_field = function(col, elem)
  6983. 6983 : {
  6984. 6984 : var label = this.env.coltypes[col].label;
  6985. 6985 :
  6986. 6986 : if (!elem)
  6987. 6987 : elem = $('.ff_' + col);
  6988. 6988 :
  6989. 6989 : if (label && !$('label[for="ff_' + col + '"]').length)
  6990. 6990 : elem.placeholder(label);
  6991. 6991 : };
  6992. 6992 :
  6993. 6993 : this.insert_edit_field = function(col, section, menu)
  6994. 6994 : {
  6995. 6995 : // just make pre-defined input field visible
  6996. 6996 : var elem = $('#ff_' + col);
  6997. 6997 : if (elem.length) {
  6998. 6998 : $('label[for="ff_' + col + '"]').parent().show();
  6999. 6999 : elem.show().focus();
  7000. 7000 : $(menu).children('option[value="' + col + '"]').prop('disabled', true);
  7001. 7001 : }
  7002. 7002 : else {
  7003. 7003 : var lastelem = $('.ff_' + col),
  7004. 7004 : appendcontainer = $('#contactsection'+section+' .contactcontroller'+col);
  7005. 7005 :
  7006. 7006 : if (!appendcontainer.length) {
  7007. 7007 : var sect = $('#contactsection'+section),
  7008. 7008 : lastgroup = $('.contactfieldgroup', sect).last();
  7009. 7009 : appendcontainer = $('<fieldset>').addClass('contactfieldgroup contactcontroller'+col);
  7010. 7010 : if (lastgroup.length)
  7011. 7011 : appendcontainer.insertAfter(lastgroup);
  7012. 7012 : else
  7013. 7013 : sect.prepend(appendcontainer);
  7014. 7014 : }
  7015. 7015 :
  7016. 7016 : if (appendcontainer.get(0).nodeName == 'FIELDSET') {
  7017. 7017 : var label, input,
  7018. 7018 : colprop = this.env.coltypes[col],
  7019. 7019 : name_suffix = colprop.limit != 1 ? '[]' : '',
  7020. 7020 : compact = $(menu).data('compact') ? true : false,
  7021. 7021 : input_id = 'ff_' + col + (colprop.count || 0),
  7022. 7022 : row = $('<div>').addClass('row input-group'),
  7023. 7023 : cell = $('<div>').addClass('contactfieldcontent ' + colprop.type);
  7024. 7024 :
  7025. 7025 : // Field label
  7026. 7026 : if (colprop.subtypes_select) {
  7027. 7027 : label = $(colprop.subtypes_select);
  7028. 7028 : if (!compact)
  7029. 7029 : label = $('<div>').addClass('contactfieldlabel label').append(label);
  7030. 7030 : else
  7031. 7031 : label.addClass('input-group-prepend');
  7032. 7032 : }
  7033. 7033 : else {
  7034. 7034 : label = $('<label>').addClass('contactfieldlabel label input-group-text')
  7035. 7035 : .attr('for', input_id).text(colprop.label);
  7036. 7036 :
  7037. 7037 : if (compact)
  7038. 7038 : label = $('<span class="input-group-prepend">').append(label);
  7039. 7039 : }
  7040. 7040 :
  7041. 7041 : // Field input
  7042. 7042 : if (colprop.type == 'text' || colprop.type == 'date') {
  7043. 7043 : input = $('<input>')
  7044. 7044 : .addClass('form-control ff_' + col)
  7045. 7045 : .attr({type: 'text', name: '_'+col+name_suffix, size: colprop.size, id: input_id});
  7046. 7046 :
  7047. 7047 : this.init_edit_field(col, input);
  7048. 7048 :
  7049. 7049 : if (colprop.type == 'date' && $.datepicker)
  7050. 7050 : input.addClass('datepicker').datepicker();
  7051. 7051 : }
  7052. 7052 : else if (colprop.type == 'textarea') {
  7053. 7053 : input = $('<textarea>')
  7054. 7054 : .addClass('form-control ff_' + col)
  7055. 7055 : .attr({ name: '_' + col + name_suffix, cols: colprop.size, rows: colprop.rows, id: input_id });
  7056. 7056 :
  7057. 7057 : this.init_edit_field(col, input);
  7058. 7058 : }
  7059. 7059 : else if (colprop.type == 'composite') {
  7060. 7060 : var i, childcol, cp, first, templ, cols = [], suffices = [], content = cell;
  7061. 7061 :
  7062. 7062 : row.addClass('composite');
  7063. 7063 :
  7064. 7064 : if (compact)
  7065. 7065 : content = $('<div class="content input-group-text">');
  7066. 7066 :
  7067. 7067 : // read template for composite field order
  7068. 7068 : if (templ = this.env[col + '_template']) {
  7069. 7069 : for (i=0; i < templ.length; i++) {
  7070. 7070 : cols.push(templ[i][1]);
  7071. 7071 : suffices.push(templ[i][2]);
  7072. 7072 : }
  7073. 7073 : }
  7074. 7074 : else { // list fields according to appearance in colprop
  7075. 7075 : for (childcol in colprop.childs)
  7076. 7076 : cols.push(childcol);
  7077. 7077 : }
  7078. 7078 :
  7079. 7079 : for (i=0; i < cols.length; i++) {
  7080. 7080 : childcol = cols[i];
  7081. 7081 : cp = colprop.childs[childcol];
  7082. 7082 : input = $('<input>')
  7083. 7083 : .addClass('form-control ff_' + childcol)
  7084. 7084 : .attr({ type: 'text', name: '_' + childcol + name_suffix, size: cp.size })
  7085. 7085 : .appendTo(content);
  7086. 7086 :
  7087. 7087 : if (!compact)
  7088. 7088 : content.append(suffices[i] || " ");
  7089. 7089 :
  7090. 7090 : this.init_edit_field(childcol, input);
  7091. 7091 : if (!first) first = input;
  7092. 7092 : }
  7093. 7093 :
  7094. 7094 : if (compact)
  7095. 7095 : input = content;
  7096. 7096 : else
  7097. 7097 : input = first; // set focus to the first of this composite fields
  7098. 7098 : }
  7099. 7099 : else if (colprop.type == 'select') {
  7100. 7100 : input = $('<select>')
  7101. 7101 : .addClass('custom-select ff_' + col)
  7102. 7102 : .attr({ name: '_' + col + name_suffix, id: input_id });
  7103. 7103 :
  7104. 7104 : var options = input.attr('options');
  7105. 7105 : options[options.length] = new Option('---', '');
  7106. 7106 : if (colprop.options)
  7107. 7107 : $.each(colprop.options, function(i, val) { options[options.length] = new Option(val, i); });
  7108. 7108 : }
  7109. 7109 :
  7110. 7110 : if (input) {
  7111. 7111 : var delbutton = $('<a href="#del"></a>')
  7112. 7112 : .addClass('contactfieldbutton deletebutton input-group-text icon delete')
  7113. 7113 : .attr({title: this.get_label('delete'), rel: col})
  7114. 7114 : .html(this.env.delbutton)
  7115. 7115 : .click(function() { ref.delete_edit_field(this); return false; });
  7116. 7116 :
  7117. 7117 : row.append(label);
  7118. 7118 :
  7119. 7119 : if (!compact) {
  7120. 7120 : if (colprop.type != 'composite')
  7121. 7121 : cell.append(input);
  7122. 7122 : row.append(cell.append(delbutton));
  7123. 7123 : }
  7124. 7124 : else {
  7125. 7125 : row.append(input).append(delbutton);
  7126. 7126 : delbutton.wrap('<span class="input-group-append">');
  7127. 7127 : }
  7128. 7128 :
  7129. 7129 : row.appendTo(appendcontainer.show());
  7130. 7130 :
  7131. 7131 : if (input.is('div'))
  7132. 7132 : input.find('input').first().focus();
  7133. 7133 : else
  7134. 7134 : input.first().focus();
  7135. 7135 :
  7136. 7136 : // disable option if limit reached
  7137. 7137 : if (!colprop.count) colprop.count = 0;
  7138. 7138 : if (++colprop.count == colprop.limit && colprop.limit)
  7139. 7139 : $(menu).children('option[value="' + col + '"]').prop('disabled', true);
  7140. 7140 :
  7141. 7141 : this.triggerEvent('insert-edit-field', input);
  7142. 7142 : }
  7143. 7143 : }
  7144. 7144 : }
  7145. 7145 : };
  7146. 7146 :
  7147. 7147 : this.delete_edit_field = function(elem)
  7148. 7148 : {
  7149. 7149 : var col = $(elem).attr('rel'),
  7150. 7150 : colprop = this.env.coltypes[col],
  7151. 7151 : input_group = $(elem).parents('div.row'),
  7152. 7152 : fieldset = $(elem).parents('fieldset.contactfieldgroup'),
  7153. 7153 : addmenu = fieldset.parent().find('select.addfieldmenu');
  7154. 7154 :
  7155. 7155 : // just clear input but don't hide the last field
  7156. 7156 : if (--colprop.count <= 0 && colprop.visible)
  7157. 7157 : input_group.find('input').val('').blur();
  7158. 7158 : else {
  7159. 7159 : input_group.remove();
  7160. 7160 : // hide entire fieldset if no more rows
  7161. 7161 : if (!fieldset.children('div.row').length)
  7162. 7162 : fieldset.hide();
  7163. 7163 : }
  7164. 7164 :
  7165. 7165 : // enable option in add-field selector or insert it if necessary
  7166. 7166 : if (addmenu.length) {
  7167. 7167 : var option = addmenu.children('option[value="'+col+'"]');
  7168. 7168 : if (option.length)
  7169. 7169 : option.prop('disabled', false);
  7170. 7170 : else
  7171. 7171 : option = $('<option>').attr('value', col).html(colprop.label).appendTo(addmenu);
  7172. 7172 : addmenu.show();
  7173. 7173 : }
  7174. 7174 : };
  7175. 7175 :
  7176. 7176 : this.upload_contact_photo = function(form)
  7177. 7177 : {
  7178. 7178 : if (form && form.elements._photo.value) {
  7179. 7179 : this.async_upload_form(form, 'upload-photo', function(e) {
  7180. 7180 : ref.set_busy(false, null, ref.file_upload_id);
  7181. 7181 : });
  7182. 7182 :
  7183. 7183 : // display upload indicator
  7184. 7184 : this.file_upload_id = this.set_busy(true, 'uploading');
  7185. 7185 : }
  7186. 7186 : };
  7187. 7187 :
  7188. 7188 : this.replace_contact_photo = function(id)
  7189. 7189 : {
  7190. 7190 : var img_src = id == '-del-' ? this.env.photo_placeholder :
  7191. 7191 : this.env.comm_path + '&_action=photo&_source=' + this.env.source + '&_cid=' + (this.env.cid || 0) + '&_photo=' + id;
  7192. 7192 :
  7193. 7193 : this.set_photo_actions(id);
  7194. 7194 : $(this.gui_objects.contactphoto).children('img').attr('src', img_src);
  7195. 7195 : };
  7196. 7196 :
  7197. 7197 : this.photo_upload_end = function()
  7198. 7198 : {
  7199. 7199 : this.set_busy(false, null, this.file_upload_id);
  7200. 7200 : delete this.file_upload_id;
  7201. 7201 : };
  7202. 7202 :
  7203. 7203 : this.set_photo_actions = function(id)
  7204. 7204 : {
  7205. 7205 : var n, buttons = this.buttons['upload-photo'];
  7206. 7206 : for (n=0; buttons && n < buttons.length; n++)
  7207. 7207 : $('a#'+buttons[n].id).html(this.get_label(id == '-del-' ? 'addphoto' : 'replacephoto'));
  7208. 7208 :
  7209. 7209 : $('#ff_photo').val(id);
  7210. 7210 : this.enable_command('upload-photo', this.env.coltypes.photo ? true : false);
  7211. 7211 : this.enable_command('delete-photo', this.env.coltypes.photo && id != '-del-');
  7212. 7212 : };
  7213. 7213 :
  7214. 7214 : // load advanced search page
  7215. 7215 : this.advanced_search = function()
  7216. 7216 : {
  7217. 7217 : var dialog = $('<iframe>').attr('src', this.url('search', {_form: 1, _framed: 1})),
  7218. 7218 : search_func = function() {
  7219. 7219 : var valid = false, form = {_adv: 1};
  7220. 7220 :
  7221. 7221 : $.each($(dialog[0].contentWindow.rcmail.gui_objects.editform).serializeArray(), function() {
  7222. 7222 : if (this.name.match(/^_search/) && this.value != '') {
  7223. 7223 : form[this.name] = this.value;
  7224. 7224 : valid = true;
  7225. 7225 : }
  7226. 7226 : });
  7227. 7227 :
  7228. 7228 : if (valid) {
  7229. 7229 : ref.http_post('search', form, ref.set_busy(true, 'searching'));
  7230. 7230 : return true;
  7231. 7231 : }
  7232. 7232 : };
  7233. 7233 :
  7234. 7234 : this.simple_dialog(dialog, this.gettext('advsearch'), search_func, {
  7235. 7235 : button: 'search',
  7236. 7236 : width: 600,
  7237. 7237 : height: 500
  7238. 7238 : });
  7239. 7239 :
  7240. 7240 : return true;
  7241. 7241 : };
  7242. 7242 :
  7243. 7243 : // unselect directory/group
  7244. 7244 : this.unselect_directory = function()
  7245. 7245 : {
  7246. 7246 : this.select_folder('');
  7247. 7247 : this.enable_command('search-delete', false);
  7248. 7248 : };
  7249. 7249 :
  7250. 7250 : // callback for creating a new saved search record
  7251. 7251 : this.insert_saved_search = function(name, id)
  7252. 7252 : {
  7253. 7253 : var key = 'S'+id,
  7254. 7254 : link = $('<a>').attr({href: '#', rel: id})
  7255. 7255 : .click(function() { return ref.command('listsearch', id, this); })
  7256. 7256 : .html(name),
  7257. 7257 : prop = { name:name, id:id };
  7258. 7258 :
  7259. 7259 : this.savedsearchlist.insert({ id:key, html:link, classes:['contactsearch'] }, null, 'contactsearch');
  7260. 7260 : this.select_folder(key,'',true);
  7261. 7261 : this.enable_command('search-delete', true);
  7262. 7262 : this.env.search_id = id;
  7263. 7263 :
  7264. 7264 : this.triggerEvent('abook_search_insert', prop);
  7265. 7265 : };
  7266. 7266 :
  7267. 7267 : // creates a dialog for saved search
  7268. 7268 : this.search_create = function()
  7269. 7269 : {
  7270. 7270 : var input = $('<input>').attr('type', 'text'),
  7271. 7271 : content = $('<label>').text(this.get_label('namex')).append(input);
  7272. 7272 :
  7273. 7273 : this.simple_dialog(content, 'searchsave',
  7274. 7274 : function() {
  7275. 7275 : var name;
  7276. 7276 : if (name = input.val()) {
  7277. 7277 : ref.http_post('search-create', {_search: ref.env.search_request, /* PAMELA - Search contacts by source */_source: rcmail.env.source, _name: name},
  7278. 7278 : ref.set_busy(true, 'loading'));
  7279. 7279 : return true;
  7280. 7280 : }
  7281. 7281 : }
  7282. 7282 : );
  7283. 7283 : };
  7284. 7284 :
  7285. 7285 : this.search_delete = function()
  7286. 7286 : {
  7287. 7287 : if (this.env.search_request) {
  7288. 7288 : var lock = this.set_busy(true, 'savedsearchdeleting');
  7289. 7289 : this.http_post('search-delete', {_sid: this.env.search_id}, lock);
  7290. 7290 : }
  7291. 7291 : };
  7292. 7292 :
  7293. 7293 : // callback from server upon search-delete command
  7294. 7294 : this.remove_search_item = function(id)
  7295. 7295 : {
  7296. 7296 : if (this.savedsearchlist.remove('S' + id)) {
  7297. 7297 : this.triggerEvent('search_delete', {id: id});
  7298. 7298 : }
  7299. 7299 :
  7300. 7300 : this.env.search_id = null;
  7301. 7301 : this.env.search_request = null;
  7302. 7302 : this.list_contacts_clear();
  7303. 7303 : this.reset_qsearch();
  7304. 7304 : this.enable_command('search-delete', 'search-create', false);
  7305. 7305 : };
  7306. 7306 :
  7307. 7307 : this.listsearch = function(id)
  7308. 7308 : {
  7309. 7309 : var lock = this.set_busy(true, 'searching');
  7310. 7310 :
  7311. 7311 : if (this.contact_list) {
  7312. 7312 : this.list_contacts_clear();
  7313. 7313 : }
  7314. 7314 :
  7315. 7315 : this.reset_qsearch();
  7316. 7316 :
  7317. 7317 : if (this.savedsearchlist) {
  7318. 7318 : this.treelist.select('');
  7319. 7319 : this.savedsearchlist.select('S'+id);
  7320. 7320 : }
  7321. 7321 : else
  7322. 7322 : this.select_folder('S'+id, '', true);
  7323. 7323 :
  7324. 7324 : // reset vars
  7325. 7325 : this.env.current_page = 1;
  7326. 7326 : this.http_request('search', {_sid: id}, lock);
  7327. 7327 : };
  7328. 7328 :
  7329. 7329 : // display a dialog with QR code image
  7330. 7330 : this.qrcode = function()
  7331. 7331 : {
  7332. 7332 : var title = this.get_label('qrcode'),
  7333. 7333 : options = {button: false, cancel_button: 'close', width: 300, height: 300},
  7334. 7334 : img = new Image(300, 300);
  7335. 7335 :
  7336. 7336 : img.src = this.url('addressbook/qrcode', {_source: this.env.source, _cid: this.get_single_cid()});
  7337. 7337 :
  7338. 7338 : return this.simple_dialog(img, title, null, options);
  7339. 7339 : };
  7340. 7340 :
  7341. 7341 :
  7342. 7342 : /*********************************************************/
  7343. 7343 : /********* user settings methods *********/
  7344. 7344 : /*********************************************************/
  7345. 7345 :
  7346. 7346 : // preferences section select and load options frame
  7347. 7347 : this.section_select = function(list)
  7348. 7348 : {
  7349. 7349 : var win, id = list.get_single_selection();
  7350. 7350 :
  7351. 7351 : if (id && (win = this.get_frame_window(this.env.contentframe))) {
  7352. 7352 : this.location_href({_action: 'edit-prefs', _section: id, _framed: 1}, win, true);
  7353. 7353 : }
  7354. 7354 : };
  7355. 7355 :
  7356. 7356 : this.response_select = function(list)
  7357. 7357 : {
  7358. 7358 : var id = list.get_single_selection();
  7359. 7359 :
  7360. 7360 : this.enable_command('delete', !!id && $.inArray(id, this.env.readonly_responses) < 0);
  7361. 7361 :
  7362. 7362 : if (id) {
  7363. 7363 : this.load_response(id, 'edit-response');
  7364. 7364 : }
  7365. 7365 : };
  7366. 7366 :
  7367. 7367 : // load response record
  7368. 7368 : this.load_response = function(id, action)
  7369. 7369 : {
  7370. 7370 : var win;
  7371. 7371 :
  7372. 7372 : if (win = this.get_frame_window(this.env.contentframe)) {
  7373. 7373 : if (id || action == 'add-response') {
  7374. 7374 : if (!id)
  7375. 7375 : this.responses_list.clear_selection();
  7376. 7376 :
  7377. 7377 : this.location_href({_action: action, _key: id, _framed: 1}, win, true);
  7378. 7378 : }
  7379. 7379 : }
  7380. 7380 : };
  7381. 7381 :
  7382. 7382 : this.identity_select = function(list)
  7383. 7383 : {
  7384. 7384 : var id = list.get_single_selection();
  7385. 7385 :
  7386. 7386 : this.enable_command('delete', !!id && list.rowcount > 1 && this.env.identities_level < 2);
  7387. 7387 :
  7388. 7388 : if (id) {
  7389. 7389 : this.load_identity(id, 'edit-identity');
  7390. 7390 : }
  7391. 7391 : };
  7392. 7392 :
  7393. 7393 : // load identity record
  7394. 7394 : this.load_identity = function(id, action)
  7395. 7395 : {
  7396. 7396 : var win;
  7397. 7397 :
  7398. 7398 : if (win = this.get_frame_window(this.env.contentframe)) {
  7399. 7399 : if (id || action == 'add-identity') {
  7400. 7400 : if (!id)
  7401. 7401 : this.identity_list.clear_selection();
  7402. 7402 :
  7403. 7403 : this.location_href({_action: action, _iid: id, _framed: 1}, win, true);
  7404. 7404 : }
  7405. 7405 : }
  7406. 7406 : };
  7407. 7407 :
  7408. 7408 : this.delete_identity = function(id)
  7409. 7409 : {
  7410. 7410 : // exit if no identity is specified or if selection is empty
  7411. 7411 : var selection = this.identity_list.get_selection();
  7412. 7412 : if (!(selection.length || this.env.iid))
  7413. 7413 : return;
  7414. 7414 :
  7415. 7415 : if (!id)
  7416. 7416 : id = this.env.iid ? this.env.iid : selection[0];
  7417. 7417 :
  7418. 7418 : // submit request with appended token
  7419. 7419 : if (id) {
  7420. 7420 : this.confirm_dialog(this.get_label('deleteidentityconfirm'), 'delete', function() {
  7421. 7421 : ref.http_post('settings/delete-identity', { _iid: id }, true);
  7422. 7422 : });
  7423. 7423 : }
  7424. 7424 : };
  7425. 7425 :
  7426. 7426 : this.update_identity_row = function(id, name, add)
  7427. 7427 : {
  7428. 7428 : var list = this.identity_list,
  7429. 7429 : rid = this.html_identifier(id);
  7430. 7430 :
  7431. 7431 : if (add) {
  7432. 7432 : list.insert_row({ id:'rcmrow'+rid, cols:[ { className:'mail', innerHTML:name } ] });
  7433. 7433 : list.select(rid);
  7434. 7434 : }
  7435. 7435 : else {
  7436. 7436 : list.update_row(rid, [ name ]);
  7437. 7437 : }
  7438. 7438 : };
  7439. 7439 :
  7440. 7440 : this.update_response_row = function(response, oldkey)
  7441. 7441 : {
  7442. 7442 : var list = this.responses_list;
  7443. 7443 :
  7444. 7444 : if (list && oldkey) {
  7445. 7445 : list.update_row(oldkey, [ response.name ], response.key, true);
  7446. 7446 : }
  7447. 7447 : else if (list) {
  7448. 7448 : list.insert_row({ id:'rcmrow'+response.key, cols:[ { className:'name', innerHTML:response.name } ] });
  7449. 7449 : list.select(response.key);
  7450. 7450 : }
  7451. 7451 : };
  7452. 7452 :
  7453. 7453 : this.remove_response = function(key)
  7454. 7454 : {
  7455. 7455 : var frame;
  7456. 7456 :
  7457. 7457 : if (this.env.textresponses) {
  7458. 7458 : delete this.env.textresponses[key];
  7459. 7459 : }
  7460. 7460 :
  7461. 7461 : if (this.responses_list) {
  7462. 7462 : this.responses_list.remove_row(key);
  7463. 7463 : this.show_contentframe(false);
  7464. 7464 : }
  7465. 7465 :
  7466. 7466 : this.enable_command('delete', false);
  7467. 7467 : };
  7468. 7468 :
  7469. 7469 : this.remove_identity = function(id)
  7470. 7470 : {
  7471. 7471 : var frame, list = this.identity_list,
  7472. 7472 : rid = this.html_identifier(id);
  7473. 7473 :
  7474. 7474 : if (list && id) {
  7475. 7475 : list.remove_row(rid);
  7476. 7476 : this.show_contentframe(false);
  7477. 7477 : }
  7478. 7478 :
  7479. 7479 : this.enable_command('delete', false);
  7480. 7480 : };
  7481. 7481 :
  7482. 7482 :
  7483. 7483 : /*********************************************************/
  7484. 7484 : /********* folder manager methods *********/
  7485. 7485 : /*********************************************************/
  7486. 7486 :
  7487. 7487 : this.init_subscription_list = function()
  7488. 7488 : {
  7489. 7489 : var delim = RegExp.escape(this.env.delimiter);
  7490. 7490 :
  7491. 7491 : this.last_sub_rx = RegExp('['+delim+']?[^'+delim+']+$');
  7492. 7492 :
  7493. 7493 : this.subscription_list = new rcube_treelist_widget(this.gui_objects.subscriptionlist, {
  7494. 7494 : selectable: true,
  7495. 7495 : tabexit: false,
  7496. 7496 : parent_focus: true,
  7497. 7497 : id_prefix: 'rcmli',
  7498. 7498 : id_encode: this.html_identifier_encode,
  7499. 7499 : id_decode: this.html_identifier_decode,
  7500. 7500 : searchbox: '#foldersearch'
  7501. 7501 : });
  7502. 7502 :
  7503. 7503 : this.subscription_list
  7504. 7504 : .addEventListener('select', function(node) { ref.subscription_select(node.id); })
  7505. 7505 : .addEventListener('collapse', function(node) { ref.folder_collapsed(node) })
  7506. 7506 : .addEventListener('expand', function(node) { ref.folder_collapsed(node) })
  7507. 7507 : .addEventListener('search', function(p) { if (p.query) ref.subscription_select(); })
  7508. 7508 : .draggable({cancel: 'li.mailbox.root,input,div.treetoggle,.custom-control'})
  7509. 7509 : .droppable({
  7510. 7510 : // @todo: find better way, accept callback is executed for every folder
  7511. 7511 : // on the list when dragging starts (and stops), this is slow, but
  7512. 7512 : // I didn't find a method to check droptarget on over event
  7513. 7513 : accept: function(node) {
  7514. 7514 : if (!node.is('.mailbox'))
  7515. 7515 : return false;
  7516. 7516 :
  7517. 7517 : var source_folder = ref.folder_id2name(node.attr('id')),
  7518. 7518 : dest_folder = ref.folder_id2name(this.id),
  7519. 7519 : source = ref.env.subscriptionrows[source_folder],
  7520. 7520 : dest = ref.env.subscriptionrows[dest_folder];
  7521. 7521 :
  7522. 7522 : return source && !source[2]
  7523. 7523 : && dest_folder != source_folder.replace(ref.last_sub_rx, '')
  7524. 7524 : && !dest_folder.startsWith(source_folder + ref.env.delimiter);
  7525. 7525 : },
  7526. 7526 : drop: function(e, ui) {
  7527. 7527 : var source = ref.folder_id2name(ui.draggable.attr('id')),
  7528. 7528 : dest = ref.folder_id2name(this.id);
  7529. 7529 :
  7530. 7530 : ref.subscription_move_folder(source, dest);
  7531. 7531 : }
  7532. 7532 : });
  7533. 7533 : };
  7534. 7534 :
  7535. 7535 : this.folder_id2name = function(id)
  7536. 7536 : {
  7537. 7537 : return id ? ref.html_identifier_decode(id.replace(/^rcmli/, '')) : null;
  7538. 7538 : };
  7539. 7539 :
  7540. 7540 : this.subscription_select = function(id)
  7541. 7541 : {
  7542. 7542 : var folder;
  7543. 7543 :
  7544. 7544 : if (id && id != '*' && (folder = this.env.subscriptionrows[id])) {
  7545. 7545 : this.env.mailbox = id;
  7546. 7546 : this.show_folder(id);
  7547. 7547 : this.enable_command('delete-folder', !folder[2]);
  7548. 7548 : }
  7549. 7549 : else {
  7550. 7550 : this.env.mailbox = null;
  7551. 7551 : this.show_contentframe(false);
  7552. 7552 : this.enable_command('delete-folder', 'purge', false);
  7553. 7553 : }
  7554. 7554 : };
  7555. 7555 :
  7556. 7556 : this.subscription_move_folder = function(from, to)
  7557. 7557 : {
  7558. 7558 : if (from && to !== null && from != to && to != from.replace(this.last_sub_rx, '')) {
  7559. 7559 : var path = from.split(this.env.delimiter),
  7560. 7560 : basename = path.pop(),
  7561. 7561 : newname = to === '' || to === '*' ? basename : to + this.env.delimiter + basename;
  7562. 7562 :
  7563. 7563 : if (newname != from) {
  7564. 7564 : this.confirm_dialog(this.get_label('movefolderconfirm'), 'move', function() {
  7565. 7565 : ref.http_post('rename-folder', {_folder_oldname: from, _folder_newname: newname},
  7566. 7566 : ref.set_busy(true, 'foldermoving'));
  7567. 7567 : }, {button_class: 'save move'});
  7568. 7568 : }
  7569. 7569 : }
  7570. 7570 : };
  7571. 7571 :
  7572. 7572 : // tell server to create and subscribe a new mailbox
  7573. 7573 : this.create_folder = function()
  7574. 7574 : {
  7575. 7575 : this.show_folder('', this.env.mailbox);
  7576. 7576 : };
  7577. 7577 :
  7578. 7578 : // delete a specific mailbox with all its messages
  7579. 7579 : this.delete_folder = function(name)
  7580. 7580 : {
  7581. 7581 : if (!name)
  7582. 7582 : name = this.env.mailbox;
  7583. 7583 :
  7584. 7584 : if (name) {
  7585. 7585 : this.confirm_dialog(this.get_label('deletefolderconfirm'), 'delete', function() {
  7586. 7586 : ref.http_post('delete-folder', {_mbox: name}, ref.set_busy(true, 'folderdeleting'));
  7587. 7587 : });
  7588. 7588 : }
  7589. 7589 : };
  7590. 7590 :
  7591. 7591 : // Add folder row to the table and initialize it
  7592. 7592 : this.add_folder_row = function (id, name, display_name, is_protected, subscribed, class_name, refrow, subfolders)
  7593. 7593 : {
  7594. 7594 : if (!this.gui_objects.subscriptionlist)
  7595. 7595 : return false;
  7596. 7596 :
  7597. 7597 : // reset searching
  7598. 7598 : if (this.subscription_list.is_search()) {
  7599. 7599 : this.subscription_select();
  7600. 7600 : this.subscription_list.reset_search();
  7601. 7601 : }
  7602. 7602 :
  7603. 7603 : // disable drag-n-drop temporarily
  7604. 7604 : // some skins disable dragging in mobile mode, so we have to check if it is still draggable
  7605. 7605 : if (this.subscription_list.is_draggable())
  7606. 7606 : this.subscription_list.draggable('destroy').droppable('destroy');
  7607. 7607 :
  7608. 7608 : var row, n, tmp, tmp_name, rowid, collator, pos, p, parent = '',
  7609. 7609 : folders = [], list = [], slist = [],
  7610. 7610 : list_element = $(this.gui_objects.subscriptionlist);
  7611. 7611 : row = refrow ? refrow : $($('li', list_element).get(1)).clone(true);
  7612. 7612 :
  7613. 7613 : if (!row.length) {
  7614. 7614 : // Refresh page if we don't have a table row to clone
  7615. 7615 : this.goto_url('folders');
  7616. 7616 : return false;
  7617. 7617 : }
  7618. 7618 :
  7619. 7619 : // set ID, reset css class
  7620. 7620 : row.attr({id: 'rcmli' + this.html_identifier_encode(id), 'class': class_name});
  7621. 7621 :
  7622. 7622 : if (!refrow || !refrow.length) {
  7623. 7623 : // remove old data, subfolders and toggle
  7624. 7624 : $('ul,div.treetoggle', row).remove();
  7625. 7625 : row.removeData('filtered');
  7626. 7626 : }
  7627. 7627 :
  7628. 7628 : // set folder name
  7629. 7629 : $('a', row).first().text(display_name).removeAttr('title');
  7630. 7630 :
  7631. 7631 : // update subscription checkbox
  7632. 7632 : $('input[name="_subscribed[]"]', row).first().val(id)
  7633. 7633 : .prop({checked: subscribed ? true : false, disabled: is_protected ? true : false});
  7634. 7634 :
  7635. 7635 : // add to folder/row-ID map
  7636. 7636 : this.env.subscriptionrows[id] = [name, display_name, false];
  7637. 7637 :
  7638. 7638 : // copy folders data to an array for sorting
  7639. 7639 : $.each(this.env.subscriptionrows, function(k, v) { v[3] = k; folders.push(v); });
  7640. 7640 :
  7641. 7641 : try {
  7642. 7642 : // use collator if supported (FF29, IE11, Opera15, Chrome24)
  7643. 7643 : collator = new Intl.Collator(this.env.locale.replace('_', '-'));
  7644. 7644 : }
  7645. 7645 : catch (e) {};
  7646. 7646 :
  7647. 7647 : // sort folders
  7648. 7648 : folders.sort(function(a, b) {
  7649. 7649 : var i, f1, f2,
  7650. 7650 : path1 = a[0].split(ref.env.delimiter),
  7651. 7651 : path2 = b[0].split(ref.env.delimiter),
  7652. 7652 : len = path1.length;
  7653. 7653 :
  7654. 7654 : for (i=0; i<len; i++) {
  7655. 7655 : f1 = path1[i];
  7656. 7656 : f2 = path2[i];
  7657. 7657 :
  7658. 7658 : if (f1 !== f2) {
  7659. 7659 : if (f2 === undefined)
  7660. 7660 : return 1;
  7661. 7661 : if (collator)
  7662. 7662 : return collator.compare(f1, f2);
  7663. 7663 : else
  7664. 7664 : return f1 < f2 ? -1 : 1;
  7665. 7665 : }
  7666. 7666 : else if (i == len-1) {
  7667. 7667 : return -1
  7668. 7668 : }
  7669. 7669 : }
  7670. 7670 : });
  7671. 7671 :
  7672. 7672 : for (n in folders) {
  7673. 7673 : p = folders[n][3];
  7674. 7674 : // protected folder
  7675. 7675 : if (folders[n][2]) {
  7676. 7676 : tmp_name = p + this.env.delimiter;
  7677. 7677 : // prefix namespace cannot have subfolders (#1488349)
  7678. 7678 : if (tmp_name == this.env.prefix_ns)
  7679. 7679 : continue;
  7680. 7680 : slist.push(p);
  7681. 7681 : tmp = tmp_name;
  7682. 7682 : }
  7683. 7683 : // protected folder's child
  7684. 7684 : else if (tmp && p.startsWith(tmp))
  7685. 7685 : slist.push(p);
  7686. 7686 : // other
  7687. 7687 : else {
  7688. 7688 : list.push(p);
  7689. 7689 : tmp = null;
  7690. 7690 : }
  7691. 7691 : }
  7692. 7692 :
  7693. 7693 : // check if subfolder of a protected folder
  7694. 7694 : for (n=0; n<slist.length; n++) {
  7695. 7695 : if (id.startsWith(slist[n] + this.env.delimiter))
  7696. 7696 : rowid = slist[n];
  7697. 7697 : }
  7698. 7698 :
  7699. 7699 : // find folder position after sorting
  7700. 7700 : for (n=0; !rowid && n<list.length; n++) {
  7701. 7701 : if (n && list[n] == id)
  7702. 7702 : rowid = list[n-1];
  7703. 7703 : }
  7704. 7704 :
  7705. 7705 : // add row to the table
  7706. 7706 : if (rowid && (n = this.subscription_list.get_item(rowid, true))) {
  7707. 7707 : // find parent folder
  7708. 7708 : if (pos = id.lastIndexOf(this.env.delimiter)) {
  7709. 7709 : parent = id.substring(0, pos);
  7710. 7710 : parent = this.subscription_list.get_item(parent, true);
  7711. 7711 :
  7712. 7712 : // add required tree elements to the parent if not already there
  7713. 7713 : if (!$('div.treetoggle', parent).length) {
  7714. 7714 : $('<div>&nbsp;</div>').addClass('treetoggle collapsed').appendTo(parent);
  7715. 7715 : }
  7716. 7716 : if (!$('ul', parent).length) {
  7717. 7717 : $('<ul>').css('display', 'none').appendTo(parent);
  7718. 7718 : }
  7719. 7719 : }
  7720. 7720 :
  7721. 7721 : if (parent && n == parent) {
  7722. 7722 : $('ul', parent).first().append(row);
  7723. 7723 : }
  7724. 7724 : else {
  7725. 7725 : while (p = $(n).parent().parent().get(0)) {
  7726. 7726 : if (parent && p == parent)
  7727. 7727 : break;
  7728. 7728 : if (!$(p).is('li.mailbox'))
  7729. 7729 : break;
  7730. 7730 : n = p;
  7731. 7731 : }
  7732. 7732 :
  7733. 7733 : $(n).after(row);
  7734. 7734 : }
  7735. 7735 : }
  7736. 7736 : else {
  7737. 7737 : list_element.append(row);
  7738. 7738 : }
  7739. 7739 :
  7740. 7740 : // add subfolders
  7741. 7741 : $.extend(this.env.subscriptionrows, subfolders || {});
  7742. 7742 :
  7743. 7743 : // update list widget
  7744. 7744 : this.subscription_list.reset(true);
  7745. 7745 : this.subscription_select();
  7746. 7746 :
  7747. 7747 : // expand parent
  7748. 7748 : if (parent) {
  7749. 7749 : this.subscription_list.expand(this.folder_id2name(parent.id));
  7750. 7750 : }
  7751. 7751 :
  7752. 7752 : row = row.show().get(0);
  7753. 7753 : if (row.scrollIntoView)
  7754. 7754 : row.scrollIntoView(false);
  7755. 7755 :
  7756. 7756 : // Let skins to do their magic, e.g. Elastic will fix pretty checkbox
  7757. 7757 : if (!refrow)
  7758. 7758 : this.triggerEvent('clonerow', {row: row, id: id});
  7759. 7759 :
  7760. 7760 : return row;
  7761. 7761 : };
  7762. 7762 :
  7763. 7763 : // replace an existing table row with a new folder line (with subfolders)
  7764. 7764 : this.replace_folder_row = function(oldid, id, name, display_name, is_protected, class_name)
  7765. 7765 : {
  7766. 7766 : if (!this.gui_objects.subscriptionlist) {
  7767. 7767 : if (this.is_framed()) {
  7768. 7768 : // @FIXME: for some reason this 'parent' variable need to be prefixed with 'window.'
  7769. 7769 : return window.parent.rcmail.replace_folder_row(oldid, id, name, display_name, is_protected, class_name);
  7770. 7770 : }
  7771. 7771 :
  7772. 7772 : return false;
  7773. 7773 : }
  7774. 7774 :
  7775. 7775 : // reset searching
  7776. 7776 : if (this.subscription_list.is_search()) {
  7777. 7777 : this.subscription_select();
  7778. 7778 : this.subscription_list.reset_search();
  7779. 7779 : }
  7780. 7780 :
  7781. 7781 : var subfolders = {},
  7782. 7782 : row = this.subscription_list.get_item(oldid, true),
  7783. 7783 : parent = $(row).parent(),
  7784. 7784 : old_folder = this.env.subscriptionrows[oldid],
  7785. 7785 : prefix_len_id = oldid.length,
  7786. 7786 : prefix_len_name = old_folder[0].length,
  7787. 7787 : subscribed = $('input[name="_subscribed[]"]', row).first().prop('checked');
  7788. 7788 :
  7789. 7789 : // no renaming, only update class_name
  7790. 7790 : if (oldid == id) {
  7791. 7791 : $(row).attr('class', class_name || '');
  7792. 7792 : return;
  7793. 7793 : }
  7794. 7794 :
  7795. 7795 : // update subfolders
  7796. 7796 : $('li', row).each(function() {
  7797. 7797 : var fname = ref.folder_id2name(this.id),
  7798. 7798 : folder = ref.env.subscriptionrows[fname],
  7799. 7799 : newid = id + fname.slice(prefix_len_id);
  7800. 7800 :
  7801. 7801 : this.id = 'rcmli' + ref.html_identifier_encode(newid);
  7802. 7802 : $('input[name="_subscribed[]"]', this).first().val(newid);
  7803. 7803 : folder[0] = name + folder[0].slice(prefix_len_name);
  7804. 7804 :
  7805. 7805 : subfolders[newid] = folder;
  7806. 7806 : delete ref.env.subscriptionrows[fname];
  7807. 7807 : });
  7808. 7808 :
  7809. 7809 : // get row off the list
  7810. 7810 : row = $(row).detach();
  7811. 7811 :
  7812. 7812 : delete this.env.subscriptionrows[oldid];
  7813. 7813 :
  7814. 7814 : // remove parent list/toggle elements if not needed
  7815. 7815 : if (parent.get(0) != this.gui_objects.subscriptionlist && !$('li', parent).length) {
  7816. 7816 : $('ul,div.treetoggle', parent.parent()).remove();
  7817. 7817 : }
  7818. 7818 :
  7819. 7819 : // move the existing table row
  7820. 7820 : this.add_folder_row(id, name, display_name, is_protected, subscribed, class_name, row, subfolders);
  7821. 7821 : };
  7822. 7822 :
  7823. 7823 : // remove the table row of a specific mailbox from the table
  7824. 7824 : this.remove_folder_row = function(folder)
  7825. 7825 : {
  7826. 7826 : // reset searching
  7827. 7827 : if (this.subscription_list.is_search()) {
  7828. 7828 : this.subscription_select();
  7829. 7829 : this.subscription_list.reset_search();
  7830. 7830 : }
  7831. 7831 :
  7832. 7832 : var list = [], row = this.subscription_list.get_item(folder, true);
  7833. 7833 :
  7834. 7834 : // get subfolders if any
  7835. 7835 : $('li', row).each(function() { list.push(ref.folder_id2name(this.id)); });
  7836. 7836 :
  7837. 7837 : // remove folder row (and subfolders)
  7838. 7838 : this.subscription_list.remove(folder);
  7839. 7839 :
  7840. 7840 : // update local list variable
  7841. 7841 : list.push(folder);
  7842. 7842 : $.each(list, function(i, v) { delete ref.env.subscriptionrows[v]; });
  7843. 7843 : };
  7844. 7844 :
  7845. 7845 : this.subscribe = function(folder)
  7846. 7846 : {
  7847. 7847 : this.change_subscription_state(folder, true);
  7848. 7848 : };
  7849. 7849 :
  7850. 7850 : this.unsubscribe = function(folder)
  7851. 7851 : {
  7852. 7852 : this.change_subscription_state(folder, false);
  7853. 7853 : };
  7854. 7854 :
  7855. 7855 : this.change_subscription_state = function(folder, state)
  7856. 7856 : {
  7857. 7857 : if (folder) {
  7858. 7858 : var prefix = state ? '' : 'un',
  7859. 7859 : lock = this.display_message('folder' + prefix + 'subscribing', 'loading');
  7860. 7860 :
  7861. 7861 : this.http_post(prefix + 'subscribe', {_mbox: folder}, lock);
  7862. 7862 :
  7863. 7863 : // in case this was a list of search results, update also the main list
  7864. 7864 : $(this.gui_objects.subscriptionlist).find('input[value="' + folder + '"]').prop('checked', state);
  7865. 7865 : }
  7866. 7866 : };
  7867. 7867 :
  7868. 7868 :
  7869. 7869 : // when user select a folder in manager
  7870. 7870 : this.show_folder = function(folder, path, force)
  7871. 7871 : {
  7872. 7872 : var win, target = window,
  7873. 7873 : action = folder === '' ? 'add' : 'edit',
  7874. 7874 : url = '&_action=' + action + '-folder&_mbox=' + urlencode(folder);
  7875. 7875 :
  7876. 7876 : if (path)
  7877. 7877 : url += '&_path='+urlencode(path);
  7878. 7878 :
  7879. 7879 : if (win = this.get_frame_window(this.env.contentframe)) {
  7880. 7880 : target = win;
  7881. 7881 : url += '&_framed=1';
  7882. 7882 : }
  7883. 7883 :
  7884. 7884 : if (String(target.location.href).indexOf(url) >= 0 && !force)
  7885. 7885 : this.show_contentframe(true);
  7886. 7886 : else
  7887. 7887 : this.location_href(this.env.comm_path+url, target, true);
  7888. 7888 : };
  7889. 7889 :
  7890. 7890 : // disables subscription checkbox (for protected folder)
  7891. 7891 : this.disable_subscription = function(folder)
  7892. 7892 : {
  7893. 7893 : var row = this.subscription_list.get_item(folder, true);
  7894. 7894 : if (row)
  7895. 7895 : $('input[name="_subscribed[]"]', row).first().prop('disabled', true);
  7896. 7896 : };
  7897. 7897 :
  7898. 7898 : // resets state of subscription checkbox (e.g. on error)
  7899. 7899 : this.reset_subscription = function(folder, state)
  7900. 7900 : {
  7901. 7901 : var row = this.subscription_list.get_item(folder, true);
  7902. 7902 : if (row)
  7903. 7903 : $('input[name="_subscribed[]"]', row).first().prop('checked', state);
  7904. 7904 : };
  7905. 7905 :
  7906. 7906 : this.folder_size = function(folder)
  7907. 7907 : {
  7908. 7908 : var lock = this.set_busy(true, 'loading');
  7909. 7909 : this.http_post('folder-size', {_mbox: folder}, lock);
  7910. 7910 : };
  7911. 7911 :
  7912. 7912 : this.folder_size_update = function(size)
  7913. 7913 : {
  7914. 7914 : $('#folder-size').replaceWith(size);
  7915. 7915 : };
  7916. 7916 :
  7917. 7917 : // filter folders by namespace
  7918. 7918 : this.folder_filter = function(prefix)
  7919. 7919 : {
  7920. 7920 : this.subscription_list.reset_search();
  7921. 7921 :
  7922. 7922 : this.subscription_list.container.children('li').each(function() {
  7923. 7923 : var i, folder = ref.folder_id2name(this.id);
  7924. 7924 : // show all folders
  7925. 7925 : if (prefix == '---') {
  7926. 7926 : }
  7927. 7927 : // got namespace prefix
  7928. 7928 : else if (prefix) {
  7929. 7929 : if (folder !== prefix) {
  7930. 7930 : $(this).data('filtered', true).hide();
  7931. 7931 : return
  7932. 7932 : }
  7933. 7933 : }
  7934. 7934 : // no namespace prefix, filter out all other namespaces
  7935. 7935 : else {
  7936. 7936 : // first get all namespace roots
  7937. 7937 : for (i in ref.env.ns_roots) {
  7938. 7938 : if (folder === ref.env.ns_roots[i]) {
  7939. 7939 : $(this).data('filtered', true).hide();
  7940. 7940 : return;
  7941. 7941 : }
  7942. 7942 : }
  7943. 7943 : }
  7944. 7944 :
  7945. 7945 : $(this).removeData('filtered').show();
  7946. 7946 : });
  7947. 7947 : };
  7948. 7948 :
  7949. 7949 : /*********************************************************/
  7950. 7950 : /********* GUI functionality *********/
  7951. 7951 : /*********************************************************/
  7952. 7952 :
  7953. 7953 : this.init_button = function(cmd, prop)
  7954. 7954 : {
  7955. 7955 : var elm = document.getElementById(prop.id);
  7956. 7956 : if (!elm)
  7957. 7957 : return;
  7958. 7958 :
  7959. 7959 : var preload = false;
  7960. 7960 : if (prop.type == 'image') {
  7961. 7961 : elm = elm.parentNode;
  7962. 7962 : preload = true;
  7963. 7963 : }
  7964. 7964 :
  7965. 7965 : elm._command = cmd;
  7966. 7966 : elm._id = prop.id;
  7967. 7967 : if (prop.sel) {
  7968. 7968 : elm.onmousedown = function(e) { return ref.button_sel(this._command, this._id); };
  7969. 7969 : elm.onmouseup = function(e) { return ref.button_out(this._command, this._id); };
  7970. 7970 : if (preload)
  7971. 7971 : new Image().src = prop.sel;
  7972. 7972 : }
  7973. 7973 : if (prop.over) {
  7974. 7974 : elm.onmouseover = function(e) { return ref.button_over(this._command, this._id); };
  7975. 7975 : elm.onmouseout = function(e) { return ref.button_out(this._command, this._id); };
  7976. 7976 : if (preload)
  7977. 7977 : new Image().src = prop.over;
  7978. 7978 : }
  7979. 7979 : };
  7980. 7980 :
  7981. 7981 : // set event handlers on registered buttons
  7982. 7982 : this.init_buttons = function()
  7983. 7983 : {
  7984. 7984 : for (var cmd in this.buttons) {
  7985. 7985 : if (typeof cmd !== 'string')
  7986. 7986 : continue;
  7987. 7987 :
  7988. 7988 : for (var i=0; i<this.buttons[cmd].length; i++) {
  7989. 7989 : this.init_button(cmd, this.buttons[cmd][i]);
  7990. 7990 : }
  7991. 7991 : }
  7992. 7992 : };
  7993. 7993 :
  7994. 7994 : // set button to a specific state
  7995. 7995 : this.set_button = function(command, state)
  7996. 7996 : {
  7997. 7997 : var n, button, obj, a_buttons = this.buttons[command],
  7998. 7998 : len = a_buttons ? a_buttons.length : 0;
  7999. 7999 :
  8000. 8000 : for (n=0; n<len; n++) {
  8001. 8001 : button = a_buttons[n];
  8002. 8002 : obj = document.getElementById(button.id);
  8003. 8003 :
  8004. 8004 : if (!obj || button.status === state)
  8005. 8005 : continue;
  8006. 8006 :
  8007. 8007 : // get default/passive setting of the button
  8008. 8008 : if (button.type == 'image' && !button.status) {
  8009. 8009 : button.pas = obj._original_src ? obj._original_src : obj.src;
  8010. 8010 : // respect PNG fix on IE browsers
  8011. 8011 : if (obj.runtimeStyle && obj.runtimeStyle.filter && obj.runtimeStyle.filter.match(/src=['"]([^'"]+)['"]/))
  8012. 8012 : button.pas = RegExp.$1;
  8013. 8013 : }
  8014. 8014 : else if (!button.status)
  8015. 8015 : button.pas = String(obj.className);
  8016. 8016 :
  8017. 8017 : button.status = state;
  8018. 8018 :
  8019. 8019 : // set image according to button state
  8020. 8020 : if (button.type == 'image' && button[state]) {
  8021. 8021 : obj.src = button[state];
  8022. 8022 : }
  8023. 8023 : // set class name according to button state
  8024. 8024 : else if (button[state] !== undefined) {
  8025. 8025 : obj.className = button[state];
  8026. 8026 : }
  8027. 8027 :
  8028. 8028 : // disable/enable input buttons
  8029. 8029 : if (button.type == 'input' || button.type == 'button') {
  8030. 8030 : obj.disabled = state == 'pas';
  8031. 8031 : }
  8032. 8032 : else {
  8033. 8033 : $(obj).attr({
  8034. 8034 : tabindex: state == 'pas' || state == 'sel' ? '-1' : ($(obj).attr('data-tabindex') || '0'),
  8035. 8035 : 'aria-disabled': state == 'pas' || state == 'sel' ? 'true' : 'false'
  8036. 8036 : });
  8037. 8037 : }
  8038. 8038 : }
  8039. 8039 : };
  8040. 8040 :
  8041. 8041 : // display a specific alttext
  8042. 8042 : this.set_alttext = function(command, label)
  8043. 8043 : {
  8044. 8044 : var n, button, obj, link, label,
  8045. 8045 : a_buttons = this.buttons[command],
  8046. 8046 : len = a_buttons ? a_buttons.length : 0;
  8047. 8047 :
  8048. 8048 : for (n=0; n<len; n++) {
  8049. 8049 : button = a_buttons[n];
  8050. 8050 : obj = document.getElementById(button.id);
  8051. 8051 : label = this.get_label(label);
  8052. 8052 :
  8053. 8053 : if (obj && button.type == 'image') {
  8054. 8054 : obj.setAttribute('alt', label);
  8055. 8055 : if ((link = obj.parentNode) && link.tagName.toLowerCase() == 'a')
  8056. 8056 : link.setAttribute('title', label);
  8057. 8057 : }
  8058. 8058 : else if (obj)
  8059. 8059 : obj.setAttribute('title', label);
  8060. 8060 : }
  8061. 8061 : };
  8062. 8062 :
  8063. 8063 : // mouse over button
  8064. 8064 : this.button_over = function(command, id)
  8065. 8065 : {
  8066. 8066 : this.button_event(command, id, 'over');
  8067. 8067 : };
  8068. 8068 :
  8069. 8069 : // mouse down on button
  8070. 8070 : this.button_sel = function(command, id)
  8071. 8071 : {
  8072. 8072 : this.button_event(command, id, 'sel');
  8073. 8073 : };
  8074. 8074 :
  8075. 8075 : // mouse out of button
  8076. 8076 : this.button_out = function(command, id)
  8077. 8077 : {
  8078. 8078 : this.button_event(command, id, 'act');
  8079. 8079 : };
  8080. 8080 :
  8081. 8081 : // event of button
  8082. 8082 : this.button_event = function(command, id, event)
  8083. 8083 : {
  8084. 8084 : var n, button, obj, a_buttons = this.buttons[command],
  8085. 8085 : len = a_buttons ? a_buttons.length : 0;
  8086. 8086 :
  8087. 8087 : for (n=0; n<len; n++) {
  8088. 8088 : button = a_buttons[n];
  8089. 8089 : if (button.id == id && button.status == 'act') {
  8090. 8090 : if (button[event] && (obj = document.getElementById(button.id))) {
  8091. 8091 : obj[button.type == 'image' ? 'src' : 'className'] = button[event];
  8092. 8092 : }
  8093. 8093 :
  8094. 8094 : if (event == 'sel') {
  8095. 8095 : this.buttons_sel[id] = command;
  8096. 8096 : }
  8097. 8097 : }
  8098. 8098 : }
  8099. 8099 : };
  8100. 8100 :
  8101. 8101 : // write to the document/window title
  8102. 8102 : this.set_pagetitle = function(title)
  8103. 8103 : {
  8104. 8104 : if (title && document.title)
  8105. 8105 : document.title = title;
  8106. 8106 : };
  8107. 8107 :
  8108. 8108 : // display a system message, list of types in common.css (below #message definition)
  8109. 8109 : this.display_message = function(msg, type, timeout, key)
  8110. 8110 : {
  8111. 8111 : if (msg && msg.length && /^[a-z._]+$/.test(msg))
  8112. 8112 : msg = this.get_label(msg);
  8113. 8113 :
  8114. 8114 : // pass command to parent window
  8115. 8115 : if (this.is_framed())
  8116. 8116 : return parent.rcmail.display_message(msg, type, timeout);
  8117. 8117 :
  8118. 8118 : if (!this.gui_objects.message) {
  8119. 8119 : // save message in order to display after page loaded
  8120. 8120 : if (type != 'loading')
  8121. 8121 : this.pending_message = [msg, type, timeout, key];
  8122. 8122 : return 1;
  8123. 8123 : }
  8124. 8124 :
  8125. 8125 : if (!type)
  8126. 8126 : type = 'notice';
  8127. 8127 : else if (type == 'loading') {
  8128. 8128 : if (!key)
  8129. 8129 : key = 'loading';
  8130. 8130 : if (!timeout)
  8131. 8131 : timeout = this.env.request_timeout * 1000;
  8132. 8132 : if (!msg)
  8133. 8133 : msg = this.get_label('loading');
  8134. 8134 : }
  8135. 8135 :
  8136. 8136 : if (!key)
  8137. 8137 : key = this.html_identifier(msg);
  8138. 8138 :
  8139. 8139 : var date = new Date(),
  8140. 8140 : id = type + date.getTime();
  8141. 8141 :
  8142. 8142 : if (!timeout) {
  8143. 8143 : switch (type) {
  8144. 8144 : case 'error':
  8145. 8145 : case 'warning':
  8146. 8146 : timeout = this.message_time * 2;
  8147. 8147 : break;
  8148. 8148 :
  8149. 8149 : case 'uploading':
  8150. 8150 : timeout = 0;
  8151. 8151 : break;
  8152. 8152 :
  8153. 8153 : default:
  8154. 8154 : timeout = this.message_time;
  8155. 8155 : }
  8156. 8156 : }
  8157. 8157 :
  8158. 8158 : // The same message is already displayed
  8159. 8159 : if (this.messages[key]) {
  8160. 8160 : // replace label
  8161. 8161 : if (this.messages[key].obj)
  8162. 8162 : $('div.content', this.messages[key].obj).html(msg);
  8163. 8163 : // store label in stack
  8164. 8164 : if (type == 'loading') {
  8165. 8165 : this.messages[key].labels.push({'id': id, 'msg': msg});
  8166. 8166 : }
  8167. 8167 : // add element and set timeout
  8168. 8168 : this.messages[key].elements.push(id);
  8169. 8169 : setTimeout(function() { ref.hide_message(id, type == 'loading'); }, timeout);
  8170. 8170 : return id;
  8171. 8171 : }
  8172. 8172 :
  8173. 8173 : // create DOM object and display it
  8174. 8174 : var obj = $('<div>').addClass(type + ' content').html(msg).data('key', key),
  8175. 8175 : cont = $(this.gui_objects.message).append(obj).show();
  8176. 8176 :
  8177. 8177 : this.messages[key] = {'obj': obj, 'elements': [id]};
  8178. 8178 :
  8179. 8179 : if (type == 'loading') {
  8180. 8180 : this.messages[key].labels = [{'id': id, 'msg': msg}];
  8181. 8181 : }
  8182. 8182 : else if (type != 'uploading') {
  8183. 8183 : obj.click(function() { return ref.hide_message(obj); })
  8184. 8184 : .attr('role', 'alert');
  8185. 8185 : }
  8186. 8186 :
  8187. 8187 : this.triggerEvent('message', { message:msg, type:type, timeout:timeout, object:obj });
  8188. 8188 :
  8189. 8189 : if (timeout > 0)
  8190. 8190 : setTimeout(function() { ref.hide_message(id, type != 'loading'); }, timeout);
  8191. 8191 :
  8192. 8192 : return id;
  8193. 8193 : };
  8194. 8194 :
  8195. 8195 : // make a message to disappear
  8196. 8196 : this.hide_message = function(obj, fade)
  8197. 8197 : {
  8198. 8198 : // pass command to parent window
  8199. 8199 : if (this.is_framed())
  8200. 8200 : return parent.rcmail.hide_message(obj, fade);
  8201. 8201 :
  8202. 8202 : if (!this.gui_objects.message)
  8203. 8203 : return;
  8204. 8204 :
  8205. 8205 : var k, n, i, o, m = this.messages;
  8206. 8206 :
  8207. 8207 : // Hide message by object, don't use for 'loading'!
  8208. 8208 : if (typeof obj === 'object') {
  8209. 8209 : o = $(obj);
  8210. 8210 : k = o.data('key');
  8211. 8211 : this.hide_message_object(o, fade);
  8212. 8212 : if (m[k])
  8213. 8213 : delete m[k];
  8214. 8214 : }
  8215. 8215 : // Hide message by id
  8216. 8216 : else {
  8217. 8217 : for (k in m) {
  8218. 8218 : for (n in m[k].elements) {
  8219. 8219 : if (m[k] && m[k].elements[n] == obj) {
  8220. 8220 : m[k].elements.splice(n, 1);
  8221. 8221 : // hide DOM element if last instance is removed
  8222. 8222 : if (!m[k].elements.length) {
  8223. 8223 : this.hide_message_object(m[k].obj, fade);
  8224. 8224 : delete m[k];
  8225. 8225 : }
  8226. 8226 : // set pending action label for 'loading' message
  8227. 8227 : else if (k == 'loading') {
  8228. 8228 : for (i in m[k].labels) {
  8229. 8229 : if (m[k].labels[i].id == obj) {
  8230. 8230 : delete m[k].labels[i];
  8231. 8231 : }
  8232. 8232 : else {
  8233. 8233 : o = m[k].labels[i].msg;
  8234. 8234 : $('div.content', m[k].obj).html(o);
  8235. 8235 : }
  8236. 8236 : }
  8237. 8237 : }
  8238. 8238 : }
  8239. 8239 : }
  8240. 8240 : }
  8241. 8241 : }
  8242. 8242 : };
  8243. 8243 :
  8244. 8244 : // hide message object and remove from the DOM
  8245. 8245 : this.hide_message_object = function(o, fade)
  8246. 8246 : {
  8247. 8247 : if (fade)
  8248. 8248 : o.fadeOut(600, function() { $(this).remove(); });
  8249. 8249 : else
  8250. 8250 : o.hide().remove();
  8251. 8251 : };
  8252. 8252 :
  8253. 8253 : // remove all messages immediately
  8254. 8254 : this.clear_messages = function()
  8255. 8255 : {
  8256. 8256 : // pass command to parent window
  8257. 8257 : if (this.is_framed())
  8258. 8258 : return parent.rcmail.clear_messages();
  8259. 8259 :
  8260. 8260 : var k, n, m = this.messages;
  8261. 8261 :
  8262. 8262 : for (k in m)
  8263. 8263 : for (n in m[k].elements)
  8264. 8264 : if (m[k].obj)
  8265. 8265 : this.hide_message_object(m[k].obj);
  8266. 8266 :
  8267. 8267 : this.messages = {};
  8268. 8268 : };
  8269. 8269 :
  8270. 8270 : // display uploading message with progress indicator
  8271. 8271 : // data should contain: name, total, current, percent, text
  8272. 8272 : this.display_progress = function(data)
  8273. 8273 : {
  8274. 8274 : if (!data || !data.name)
  8275. 8275 : return;
  8276. 8276 :
  8277. 8277 : var msg = this.messages['progress' + data.name];
  8278. 8278 :
  8279. 8279 : if (!data.label)
  8280. 8280 : data.label = this.get_label('uploadingmany');
  8281. 8281 :
  8282. 8282 : if (!msg) {
  8283. 8283 : if (!data.percent || data.percent < 100)
  8284. 8284 : this.display_message(data.label, 'uploading', 0, 'progress' + data.name);
  8285. 8285 : return;
  8286. 8286 : }
  8287. 8287 :
  8288. 8288 : if (!data.total || data.percent >= 100) {
  8289. 8289 : this.hide_message(msg.obj);
  8290. 8290 : return;
  8291. 8291 : }
  8292. 8292 :
  8293. 8293 : if (data.text)
  8294. 8294 : data.label += ' ' + data.text;
  8295. 8295 :
  8296. 8296 : msg.obj.text(data.label);
  8297. 8297 : };
  8298. 8298 :
  8299. 8299 : // open a jquery UI dialog with the given content
  8300. 8300 : this.show_popup_dialog = function(content, title, buttons, options)
  8301. 8301 : {
  8302. 8302 : // forward call to parent window
  8303. 8303 : if (this.is_framed()) {
  8304. 8304 : return parent.rcmail.show_popup_dialog(content, title, buttons, options);
  8305. 8305 : }
  8306. 8306 :
  8307. 8307 : var popup = $('<div class="popup">');
  8308. 8308 :
  8309. 8309 : if (typeof content == 'object') {
  8310. 8310 : popup.append(content);
  8311. 8311 : if ($(content).is('iframe'))
  8312. 8312 : popup.addClass('iframe');
  8313. 8313 : }
  8314. 8314 : else
  8315. 8315 : popup.html(content);
  8316. 8316 :
  8317. 8317 : // assign special classes to dialog buttons
  8318. 8318 : var i = 0, fn = function(button, classes, idx) {
  8319. 8319 : if (typeof button == 'function') {
  8320. 8320 : button = {
  8321. 8321 : click: button,
  8322. 8322 : text: idx,
  8323. 8323 : 'class': classes
  8324. 8324 : };
  8325. 8325 : }
  8326. 8326 : else {
  8327. 8327 : buttons['class'] = classes;
  8328. 8328 : }
  8329. 8329 :
  8330. 8330 : return button;
  8331. 8331 : };
  8332. 8332 :
  8333. 8333 : if (options && options.button_classes)
  8334. 8334 : $.each(buttons, function(idx, button) {
  8335. 8335 : var cl = options.button_classes[i];
  8336. 8336 : if (cl)
  8337. 8337 : buttons[idx] = fn(button, cl, idx);
  8338. 8338 : i++;
  8339. 8339 : });
  8340. 8340 :
  8341. 8341 : options = $.extend({
  8342. 8342 : title: title,
  8343. 8343 : buttons: buttons,
  8344. 8344 : modal: true,
  8345. 8345 : resizable: true,
  8346. 8346 : width: 500,
  8347. 8347 : close: function(event, ui) { $(this).remove(); }
  8348. 8348 : }, options || {});
  8349. 8349 :
  8350. 8350 : popup.dialog(options);
  8351. 8351 :
  8352. 8352 : if (options.width)
  8353. 8353 : popup.width(options.width);
  8354. 8354 : if (options.height)
  8355. 8355 : popup.height(options.height);
  8356. 8356 :
  8357. 8357 : var dialog = popup.parent();
  8358. 8358 :
  8359. 8359 : if (!options.noresize) {
  8360. 8360 : // resize and center popup
  8361. 8361 : var win = $(window), w = win.width(), h = win.height(),
  8362. 8362 : width = popup.width(),
  8363. 8363 : height = options.height || (popup[0].scrollHeight + 20),
  8364. 8364 : titlebar_height = $('.ui-dialog-titlebar', dialog).outerHeight() || 0,
  8365. 8365 : buttonpane_height = $('.ui-dialog-buttonpane', dialog).outerHeight() || 0,
  8366. 8366 : padding = (parseInt(dialog.css('padding-top')) + parseInt(popup.css('padding-top'))) * 2;
  8367. 8367 :
  8368. 8368 : popup.dialog('option', {
  8369. 8369 : height: Math.min(h - 40, height + titlebar_height + buttonpane_height + padding + 2),
  8370. 8370 : width: Math.min(w - 20, width + 28)
  8371. 8371 : });
  8372. 8372 : }
  8373. 8373 : else {
  8374. 8374 : popup.css('width', 'auto');
  8375. 8375 : }
  8376. 8376 :
  8377. 8377 : // Don't propagate keyboard events to the UI below the dialog (#6055)
  8378. 8378 : dialog.on('keydown keyup', function(e) { e.stopPropagation(); });
  8379. 8379 :
  8380. 8380 : // Add Enter key handler to the input, click the 'mainaction' button
  8381. 8381 : dialog.find('input[data-submit]').on('keydown', function(e) {
  8382. 8382 : if (e.which == 13) {
  8383. 8383 : dialog.find('.ui-dialog-buttonpane button.mainaction').click();
  8384. 8384 : }
  8385. 8385 : });
  8386. 8386 :
  8387. 8387 : this.triggerEvent('dialog-open', {obj: popup});
  8388. 8388 :
  8389. 8389 : return popup;
  8390. 8390 : };
  8391. 8391 :
  8392. 8392 : // show_popup_dialog() wrapper for simple dialogs with action and Cancel buttons
  8393. 8393 : this.simple_dialog = function(content, title, action_func, options)
  8394. 8394 : {
  8395. 8395 : if (!options)
  8396. 8396 : options = {};
  8397. 8397 :
  8398. 8398 : var title = this.get_label(title),
  8399. 8399 : save_label = options.button || 'save',
  8400. 8400 : save_class = options.button_class || save_label.replace(/^[^\.]+\./i, ''),
  8401. 8401 : cancel_label = options.cancel_button || 'cancel',
  8402. 8402 : cancel_class = options.cancel_class || cancel_label.replace(/^[^\.]+\./i, ''),
  8403. 8403 : close_func = function(e, ui, dialog) {
  8404. 8404 : (ref.is_framed() ? parent.$ : $)(dialog || this).dialog('close');
  8405. 8405 : if (options.cancel_func) options.cancel_func(e, ref);
  8406. 8406 : },
  8407. 8407 : buttons = [{
  8408. 8408 : text: this.get_label(cancel_label),
  8409. 8409 : 'class': cancel_class.replace(/close/i, 'cancel'),
  8410. 8410 : click: close_func
  8411. 8411 : }];
  8412. 8412 :
  8413. 8413 : if (!action_func)
  8414. 8414 : buttons[0]['class'] += ' mainaction';
  8415. 8415 : else
  8416. 8416 : buttons.unshift({
  8417. 8417 : text: this.get_label(save_label),
  8418. 8418 : 'class': 'mainaction ' + save_class,
  8419. 8419 : click: function(e, ui) { if (action_func(e, ref)) close_func(e, ui, this); }
  8420. 8420 : });
  8421. 8421 :
  8422. 8422 : return this.show_popup_dialog(content, title, buttons, options);
  8423. 8423 : };
  8424. 8424 :
  8425. 8425 : // show_popup_dialog() wrapper for alert() type dialogs
  8426. 8426 : this.alert_dialog = function(content, action, options)
  8427. 8427 : {
  8428. 8428 : options = $.extend(options || {}, {
  8429. 8429 : cancel_button: 'ok',
  8430. 8430 : cancel_class: 'save',
  8431. 8431 : cancel_func: action,
  8432. 8432 : noresize: true
  8433. 8433 : });
  8434. 8434 :
  8435. 8435 : return this.simple_dialog(content, options.title || 'alerttitle', null, options);
  8436. 8436 : };
  8437. 8437 :
  8438. 8438 : // simple_dialog() wrapper for confirm() type dialogs
  8439. 8439 : this.confirm_dialog = function(content, button_label, action, options)
  8440. 8440 : {
  8441. 8441 : var action_func = function(e, ref) { action(e, ref); return true; };
  8442. 8442 :
  8443. 8443 : options = $.extend(options || {}, {
  8444. 8444 : button: button_label || 'continue',
  8445. 8445 : noresize: true
  8446. 8446 : });
  8447. 8447 :
  8448. 8448 : return this.simple_dialog(content, options.title || 'confirmationtitle', action_func, options);
  8449. 8449 : };
  8450. 8450 :
  8451. 8451 : // enable/disable buttons for page shifting
  8452. 8452 : this.set_page_buttons = function()
  8453. 8453 : {
  8454. 8454 : this.enable_command('nextpage', 'lastpage', this.env.pagecount > this.env.current_page);
  8455. 8455 : this.enable_command('previouspage', 'firstpage', this.env.current_page > 1);
  8456. 8456 :
  8457. 8457 : this.update_pagejumper();
  8458. 8458 : };
  8459. 8459 :
  8460. 8460 : // mark a mailbox as selected and set environment variable
  8461. 8461 : this.select_folder = function(name, prefix, encode)
  8462. 8462 : {
  8463. 8463 : if (this.savedsearchlist) {
  8464. 8464 : this.savedsearchlist.select('');
  8465. 8465 : }
  8466. 8466 :
  8467. 8467 : if (this.treelist) {
  8468. 8468 : this.treelist.select(name);
  8469. 8469 : }
  8470. 8470 : else if (this.gui_objects.folderlist) {
  8471. 8471 : $('li.selected', this.gui_objects.folderlist).removeClass('selected');
  8472. 8472 : $(this.get_folder_li(name, prefix, encode)).addClass('selected');
  8473. 8473 :
  8474. 8474 : // trigger event hook
  8475. 8475 : this.triggerEvent('selectfolder', { folder:name, prefix:prefix });
  8476. 8476 : }
  8477. 8477 : };
  8478. 8478 :
  8479. 8479 : // adds a class to selected folder
  8480. 8480 : this.mark_folder = function(name, class_name, prefix, encode)
  8481. 8481 : {
  8482. 8482 : $(this.get_folder_li(name, prefix, encode)).addClass(class_name);
  8483. 8483 : this.triggerEvent('markfolder', {folder: name, mark: class_name, status: true});
  8484. 8484 : };
  8485. 8485 :
  8486. 8486 : // adds a class to selected folder
  8487. 8487 : this.unmark_folder = function(name, class_name, prefix, encode)
  8488. 8488 : {
  8489. 8489 : $(this.get_folder_li(name, prefix, encode)).removeClass(class_name);
  8490. 8490 : this.triggerEvent('markfolder', {folder: name, mark: class_name, status: false});
  8491. 8491 : };
  8492. 8492 :
  8493. 8493 : // helper method to find a folder list item
  8494. 8494 : this.get_folder_li = function(name, prefix, encode)
  8495. 8495 : {
  8496. 8496 : if (!prefix)
  8497. 8497 : prefix = 'rcmli';
  8498. 8498 :
  8499. 8499 : if (this.gui_objects.folderlist) {
  8500. 8500 : name = this.html_identifier(name, encode);
  8501. 8501 : return document.getElementById(prefix+name);
  8502. 8502 : }
  8503. 8503 : };
  8504. 8504 :
  8505. 8505 : // for reordering column array (Konqueror workaround)
  8506. 8506 : // and for setting some message list global variables
  8507. 8507 : this.set_message_coltypes = function(cols, repl, smart_col)
  8508. 8508 : {
  8509. 8509 : // update list mode columns list
  8510. 8510 : this.env.listcols = cols;
  8511. 8511 :
  8512. 8512 : // reset message list cols
  8513. 8513 : this.msglist_setup(this.env.layout);
  8514. 8514 :
  8515. 8515 : var list = this.message_list,
  8516. 8516 : thead = list ? list.thead : null,
  8517. 8517 : repl, cell, col, c, n, len, tr,
  8518. 8518 : listcols = this.env.msglist_cols;
  8519. 8519 :
  8520. 8520 : if (!this.env.coltypes)
  8521. 8521 : this.env.coltypes = {};
  8522. 8522 :
  8523. 8523 : // replace old column headers
  8524. 8524 : if (thead) {
  8525. 8525 : if (repl) {
  8526. 8526 : thead.innerHTML = '';
  8527. 8527 : tr = document.createElement('tr');
  8528. 8528 :
  8529. 8529 : for (n in listcols) {
  8530. 8530 : c = listcols[n];
  8531. 8531 : cell = document.createElement('th');
  8532. 8532 : cell.innerHTML = repl[c].html || '';
  8533. 8533 : if (repl[c].id) cell.id = repl[c].id;
  8534. 8534 : if (repl[c].className) cell.className = repl[c].className;
  8535. 8535 : tr.appendChild(cell);
  8536. 8536 : }
  8537. 8537 :
  8538. 8538 : if (list.checkbox_selection)
  8539. 8539 : list.insert_checkbox(tr, 'thead');
  8540. 8540 :
  8541. 8541 : thead.appendChild(tr);
  8542. 8542 : }
  8543. 8543 :
  8544. 8544 : for (n = 0, len = listcols.length; n < len; n++) {
  8545. 8545 : col = listcols[list.checkbox_selection ? n-1 : n];
  8546. 8546 : if ((cell = thead.rows[0].cells[n]) && (col == 'from' || col == 'to' || col == 'fromto')) {
  8547. 8547 : $(cell).attr('rel', col).find('span,a').text(this.get_label(col == 'fromto' ? smart_col : col));
  8548. 8548 : }
  8549. 8549 : }
  8550. 8550 : }
  8551. 8551 :
  8552. 8552 : this.env.subject_col = null;
  8553. 8553 : this.env.flagged_col = null;
  8554. 8554 : this.env.status_col = null;
  8555. 8555 :
  8556. 8556 : if (this.env.coltypes.folder)
  8557. 8557 : this.env.coltypes.folder.hidden = !(this.env.search_request || this.env.search_id) || this.env.search_scope == 'base';
  8558. 8558 :
  8559. 8559 : if ((n = $.inArray('subject', listcols)) >= 0) {
  8560. 8560 : this.env.subject_col = n;
  8561. 8561 : if (list)
  8562. 8562 : list.subject_col = n;
  8563. 8563 : }
  8564. 8564 : if ((n = $.inArray('flag', listcols)) >= 0)
  8565. 8565 : this.env.flagged_col = n;
  8566. 8566 : if ((n = $.inArray('status', listcols)) >= 0)
  8567. 8567 : this.env.status_col = n;
  8568. 8568 :
  8569. 8569 : if (list) {
  8570. 8570 : list.hide_column('folder', (this.env.coltypes.folder && this.env.coltypes.folder.hidden) || $.inArray('folder', listcols) < 0);
  8571. 8571 : list.init_header();
  8572. 8572 : }
  8573. 8573 : };
  8574. 8574 :
  8575. 8575 : // replace content of row count display
  8576. 8576 : this.set_rowcount = function(text, mbox)
  8577. 8577 : {
  8578. 8578 : // #1487752
  8579. 8579 : if (mbox && mbox != this.env.mailbox)
  8580. 8580 : return false;
  8581. 8581 :
  8582. 8582 : $(this.gui_objects.countdisplay).html(text);
  8583. 8583 :
  8584. 8584 : // update page navigation buttons
  8585. 8585 : this.set_page_buttons();
  8586. 8586 : };
  8587. 8587 :
  8588. 8588 : // replace content of mailboxname display
  8589. 8589 : this.set_mailboxname = function(content)
  8590. 8590 : {
  8591. 8591 : if (this.gui_objects.mailboxname && content)
  8592. 8592 : this.gui_objects.mailboxname.innerHTML = content;
  8593. 8593 : };
  8594. 8594 :
  8595. 8595 : // replace content of quota display
  8596. 8596 : this.set_quota = function(content)
  8597. 8597 : {
  8598. 8598 : if (this.gui_objects.quotadisplay && content && content.type == 'text')
  8599. 8599 : $(this.gui_objects.quotadisplay).text((content.percent||0) + '%').attr('title', content.title || '');
  8600. 8600 :
  8601. 8601 : this.triggerEvent('setquota', content);
  8602. 8602 : this.env.quota_content = content;
  8603. 8603 : };
  8604. 8604 :
  8605. 8605 : // update trash folder state
  8606. 8606 : this.set_trash_count = function(count)
  8607. 8607 : {
  8608. 8608 : this[(count ? 'un' : '') + 'mark_folder'](this.env.trash_mailbox, 'empty', '', true);
  8609. 8609 : };
  8610. 8610 :
  8611. 8611 : // update the mailboxlist
  8612. 8612 : this.set_unread_count = function(mbox, count, set_title, mark)
  8613. 8613 : {
  8614. 8614 : if (!this.gui_objects.mailboxlist)
  8615. 8615 : return false;
  8616. 8616 :
  8617. 8617 : this.env.unread_counts[mbox] = count;
  8618. 8618 : this.set_unread_count_display(mbox, set_title);
  8619. 8619 :
  8620. 8620 : if (mark)
  8621. 8621 : this.mark_folder(mbox, mark, '', true);
  8622. 8622 : else if (!count)
  8623. 8623 : this.unmark_folder(mbox, 'recent', '', true);
  8624. 8624 :
  8625. 8625 : this.mark_all_read_state();
  8626. 8626 : };
  8627. 8627 :
  8628. 8628 : // update the mailbox count display
  8629. 8629 : this.set_unread_count_display = function(mbox, set_title)
  8630. 8630 : {
  8631. 8631 : var reg, link, text_obj, item, mycount, childcount, div;
  8632. 8632 :
  8633. 8633 : if (item = this.get_folder_li(mbox, '', true)) {
  8634. 8634 : mycount = this.env.unread_counts[mbox] ? this.env.unread_counts[mbox] : 0;
  8635. 8635 : link = $(item).children('a').eq(0);
  8636. 8636 : text_obj = link.children('span.unreadcount');
  8637. 8637 : if (!text_obj.length && mycount)
  8638. 8638 : text_obj = $('<span>').addClass('unreadcount skip-content').appendTo(link);
  8639. 8639 : reg = /\s+\([0-9]+\)$/i;
  8640. 8640 :
  8641. 8641 : childcount = 0;
  8642. 8642 : if ((div = item.getElementsByTagName('div')[0]) &&
  8643. 8643 : div.className.match(/collapsed/)) {
  8644. 8644 : // add children's counters
  8645. 8645 : for (var k in this.env.unread_counts)
  8646. 8646 : if (k.startsWith(mbox + this.env.delimiter))
  8647. 8647 : childcount += this.env.unread_counts[k];
  8648. 8648 : }
  8649. 8649 :
  8650. 8650 : if (mycount && text_obj.length)
  8651. 8651 : text_obj.html(this.env.unreadwrap.replace(/%[sd]/, mycount));
  8652. 8652 : else if (text_obj.length)
  8653. 8653 : text_obj.remove();
  8654. 8654 :
  8655. 8655 : // set parent's display
  8656. 8656 : reg = new RegExp(RegExp.escape(this.env.delimiter) + '[^' + RegExp.escape(this.env.delimiter) + ']+$');
  8657. 8657 : if (mbox.match(reg))
  8658. 8658 : this.set_unread_count_display(mbox.replace(reg, ''), false);
  8659. 8659 :
  8660. 8660 : // set the right classes
  8661. 8661 : if ((mycount+childcount)>0)
  8662. 8662 : $(item).addClass('unread');
  8663. 8663 : else
  8664. 8664 : $(item).removeClass('unread');
  8665. 8665 : }
  8666. 8666 :
  8667. 8667 : // set unread count to window title
  8668. 8668 : reg = /^\([0-9]+\)\s+/i;
  8669. 8669 : if (set_title && document.title) {
  8670. 8670 : var new_title = '',
  8671. 8671 : doc_title = String(document.title);
  8672. 8672 :
  8673. 8673 : if (mycount && doc_title.match(reg))
  8674. 8674 : new_title = doc_title.replace(reg, '('+mycount+') ');
  8675. 8675 : else if (mycount)
  8676. 8676 : new_title = '('+mycount+') '+doc_title;
  8677. 8677 : else
  8678. 8678 : new_title = doc_title.replace(reg, '');
  8679. 8679 :
  8680. 8680 : this.set_pagetitle(new_title);
  8681. 8681 : }
  8682. 8682 : };
  8683. 8683 :
  8684. 8684 : // display fetched raw headers
  8685. 8685 : this.set_headers = function(content)
  8686. 8686 : {
  8687. 8687 : if (this.gui_objects.all_headers_box && content)
  8688. 8688 : $(this.gui_objects.all_headers_box).html(content).show();
  8689. 8689 : };
  8690. 8690 :
  8691. 8691 : // display all-headers row and fetch raw message headers
  8692. 8692 : this.show_headers = function(props, elem)
  8693. 8693 : {
  8694. 8694 : if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box || !this.env.uid)
  8695. 8695 : return;
  8696. 8696 :
  8697. 8697 : $(elem).removeClass('show-headers').addClass('hide-headers');
  8698. 8698 : $(this.gui_objects.all_headers_row).show();
  8699. 8699 : elem.onclick = function() { ref.command('hide-headers', '', elem); };
  8700. 8700 :
  8701. 8701 : // fetch headers only once
  8702. 8702 : if (!this.gui_objects.all_headers_box.innerHTML) {
  8703. 8703 : this.http_request('headers', {_uid: this.env.uid, _mbox: this.env.mailbox},
  8704. 8704 : this.display_message('', 'loading')
  8705. 8705 : );
  8706. 8706 : }
  8707. 8707 : };
  8708. 8708 :
  8709. 8709 : // hide all-headers row
  8710. 8710 : this.hide_headers = function(props, elem)
  8711. 8711 : {
  8712. 8712 : if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box)
  8713. 8713 : return;
  8714. 8714 :
  8715. 8715 : $(elem).removeClass('hide-headers').addClass('show-headers');
  8716. 8716 : $(this.gui_objects.all_headers_row).hide();
  8717. 8717 : elem.onclick = function() { ref.command('show-headers', '', elem); };
  8718. 8718 : };
  8719. 8719 :
  8720. 8720 : // create folder selector popup
  8721. 8721 : this.folder_selector = function(event, callback)
  8722. 8722 : {
  8723. 8723 : this.entity_selector('folder-selector', callback, this.env.mailboxes_list, function(obj, a) {
  8724. 8724 : var n = 0, s = 0,
  8725. 8725 : delim = ref.env.delimiter,
  8726. 8726 : folder = ref.env.mailboxes[obj],
  8727. 8727 : id = folder.id,
  8728. 8728 : row = $('<li>');
  8729. 8729 :
  8730. 8730 : if (folder.virtual)
  8731. 8731 : a.addClass('virtual').attr({'aria-disabled': 'true', tabindex: '-1'});
  8732. 8732 : else
  8733. 8733 : a.addClass('active').data('id', folder.id);
  8734. 8734 :
  8735. 8735 : if (folder['class'])
  8736. 8736 : row.addClass(folder['class']);
  8737. 8737 :
  8738. 8738 : // calculate/set indentation level
  8739. 8739 : while ((s = id.indexOf(delim, s)) >= 0) {
  8740. 8740 : n++; s++;
  8741. 8741 : }
  8742. 8742 : a.css('padding-left', n ? (n * 16) + 'px' : 0);
  8743. 8743 :
  8744. 8744 : // add folder name element
  8745. 8745 : a.append($('<span>').text(folder.name));
  8746. 8746 :
  8747. 8747 : return row.append(a);
  8748. 8748 : }, event);
  8749. 8749 : };
  8750. 8750 :
  8751. 8751 : // create addressbook selector popup
  8752. 8752 : this.addressbook_selector = function(event, callback)
  8753. 8753 : {
  8754. 8754 : // build addressbook + groups list
  8755. 8755 : var combined_sources = [];
  8756. 8756 :
  8757. 8757 : // check we really need it before processing
  8758. 8758 : if (!this.entity_selectors['addressbook-selector']) {
  8759. 8759 : $.each(this.env.address_sources, function() {
  8760. 8760 : if (!this.readonly) {
  8761. 8761 : var source = this;
  8762. 8762 : combined_sources.push(source);
  8763. 8763 :
  8764. 8764 : $.each(ref.env.contactgroups, function() {
  8765. 8765 : if (source.id === this.source) {
  8766. 8766 : combined_sources.push(this);
  8767. 8767 : }
  8768. 8768 : });
  8769. 8769 : }
  8770. 8770 : });
  8771. 8771 : }
  8772. 8772 :
  8773. 8773 : this.entity_selector('addressbook-selector', callback, combined_sources, function(obj, a) {
  8774. 8774 : if (obj.type == 'group') {
  8775. 8775 : a.attr('rel', obj.source + ':' + obj.id)
  8776. 8776 : .addClass('contactgroup active')
  8777. 8777 : .data({source: obj.source, gid: obj.id, id: obj.source + ':' + obj.id})
  8778. 8778 : .css('padding-left', '16px');
  8779. 8779 : }
  8780. 8780 : else {
  8781. 8781 : a.addClass('addressbook active').data('id', obj.id);
  8782. 8782 : }
  8783. 8783 : a.append($('<span>').text(obj.name));
  8784. 8784 :
  8785. 8785 : return $('<li>').append(a);
  8786. 8786 : }, event);
  8787. 8787 : };
  8788. 8788 :
  8789. 8789 : // create contactgroup selector popup
  8790. 8790 : this.contactgroup_selector = function(event, callback)
  8791. 8791 : {
  8792. 8792 : this.entity_selector('contactgroup-selector', callback, this.env.contactgroups, function(obj, a) {
  8793. 8793 : if (ref.env.source === obj.source) {
  8794. 8794 : a.addClass('contactgroup active')
  8795. 8795 : .data({id: obj.id})
  8796. 8796 : .append($('<span>').text(obj.name));
  8797. 8797 :
  8798. 8798 : return $('<li>').append(a);
  8799. 8799 : }
  8800. 8800 : }, event);
  8801. 8801 : };
  8802. 8802 :
  8803. 8803 : // create selector popup (eg for folders or address books), position and display it
  8804. 8804 : this.entity_selector = function(name, click_callback, entity_list, list_callback, event)
  8805. 8805 : {
  8806. 8806 : var container = this.entity_selectors[name];
  8807. 8807 :
  8808. 8808 : if (!container) {
  8809. 8809 : var rows = [],
  8810. 8810 : container = $('<div>').attr('id', name).addClass('popupmenu'),
  8811. 8811 : ul = $('<ul>').addClass('toolbarmenu menu'),
  8812. 8812 : link = document.createElement('a');
  8813. 8813 :
  8814. 8814 : link.href = '#';
  8815. 8815 : link.className = 'icon';
  8816. 8816 :
  8817. 8817 : // loop over entity list
  8818. 8818 : $.each(entity_list, function(i) {
  8819. 8819 : var a = $(link.cloneNode(false)).attr('rel', this.id);
  8820. 8820 : rows.push(list_callback(this, a, i));
  8821. 8821 : });
  8822. 8822 :
  8823. 8823 : ul.append(rows).appendTo(container);
  8824. 8824 :
  8825. 8825 : // temporarily show element to calculate its size
  8826. 8826 : container.css({left: '-1000px', top: '-1000px'})
  8827. 8827 : .appendTo(document.body).show();
  8828. 8828 :
  8829. 8829 : // set max-height if the list is long
  8830. 8830 : if (rows.length > 10)
  8831. 8831 : container.css('max-height', $('li', container)[0].offsetHeight * 10 + 9);
  8832. 8832 :
  8833. 8833 : // register delegate event handler for folder item clicks
  8834. 8834 : container.on('click', 'a.active', function(e) {
  8835. 8835 : container.data('callback')($(this).data('id'), this);
  8836. 8836 : });
  8837. 8837 :
  8838. 8838 : this.entity_selectors[name] = container;
  8839. 8839 : }
  8840. 8840 :
  8841. 8841 : container.data('callback', click_callback);
  8842. 8842 :
  8843. 8843 : // position menu on the screen
  8844. 8844 : this.show_menu(name, true, event);
  8845. 8845 : };
  8846. 8846 :
  8847. 8847 : this.destroy_entity_selector = function(name)
  8848. 8848 : {
  8849. 8849 : $("#" + name).remove();
  8850. 8850 : delete this.entity_selectors[name];
  8851. 8851 : this.triggerEvent('destroy-entity-selector', { name: name });
  8852. 8852 : };
  8853. 8853 :
  8854. 8854 : /***********************************************/
  8855. 8855 : /********* popup menu functions *********/
  8856. 8856 : /***********************************************/
  8857. 8857 :
  8858. 8858 : // Show/hide a specific popup menu
  8859. 8859 : this.show_menu = function(prop, show, event)
  8860. 8860 : {
  8861. 8861 : var name = typeof prop == 'object' ? prop.menu : prop,
  8862. 8862 : obj = $('#'+name),
  8863. 8863 : ref = event && event.target ? $(event.target) : $(obj.attr('rel') || '#'+name+'link'),
  8864. 8864 : keyboard = rcube_event.is_keyboard(event),
  8865. 8865 : align = obj.attr('data-align') || '',
  8866. 8866 : stack = false;
  8867. 8867 :
  8868. 8868 : // find "real" button element
  8869. 8869 : if (ref.get(0).tagName != 'A' && ref.closest('a').length)
  8870. 8870 : ref = ref.closest('a');
  8871. 8871 :
  8872. 8872 : if (typeof prop == 'string')
  8873. 8873 : prop = { menu:name };
  8874. 8874 :
  8875. 8875 : // let plugins or skins provide the menu element
  8876. 8876 : if (!obj.length) {
  8877. 8877 : obj = this.triggerEvent('menu-get', { name:name, props:prop, originalEvent:event });
  8878. 8878 : }
  8879. 8879 :
  8880. 8880 : if (!obj || !obj.length) {
  8881. 8881 : // just delegate the action to subscribers
  8882. 8882 : return this.triggerEvent(show === false ? 'menu-close' : 'menu-open', { name:name, props:prop, originalEvent:event });
  8883. 8883 : }
  8884. 8884 :
  8885. 8885 : // move element to top for proper absolute positioning
  8886. 8886 : obj.appendTo(document.body);
  8887. 8887 :
  8888. 8888 : if (typeof show == 'undefined')
  8889. 8889 : show = obj.is(':visible') ? false : true;
  8890. 8890 :
  8891. 8891 : if (show && ref.length) {
  8892. 8892 : var win = $(window),
  8893. 8893 : pos = ref.offset(),
  8894. 8894 : above = align.indexOf('bottom') >= 0;
  8895. 8895 :
  8896. 8896 : stack = ref.attr('role') == 'menuitem' || ref.closest('[role=menuitem]').length > 0;
  8897. 8897 :
  8898. 8898 : ref.offsetWidth = ref.outerWidth();
  8899. 8899 : ref.offsetHeight = ref.outerHeight();
  8900. 8900 : if (!above && pos.top + ref.offsetHeight + obj.height() > win.height()) {
  8901. 8901 : above = true;
  8902. 8902 : }
  8903. 8903 : if (align.indexOf('right') >= 0) {
  8904. 8904 : pos.left = pos.left + ref.outerWidth() - obj.width();
  8905. 8905 : }
  8906. 8906 : else if (stack) {
  8907. 8907 : pos.left = pos.left + ref.offsetWidth - 5;
  8908. 8908 : pos.top -= ref.offsetHeight;
  8909. 8909 : }
  8910. 8910 : if (pos.left + obj.width() > win.width()) {
  8911. 8911 : pos.left = win.width() - obj.width() - 12;
  8912. 8912 : }
  8913. 8913 : pos.top = Math.max(0, pos.top + (above ? -obj.height() : ref.offsetHeight));
  8914. 8914 : obj.css({ left:pos.left+'px', top:pos.top+'px' });
  8915. 8915 : }
  8916. 8916 :
  8917. 8917 : // add menu to stack
  8918. 8918 : if (show) {
  8919. 8919 : // truncate stack down to the one containing the ref link
  8920. 8920 : for (var i = this.menu_stack.length - 1; stack && i >= 0; i--) {
  8921. 8921 : if (!$(ref).parents('#'+this.menu_stack[i]).length && $(event.target).parent().attr('role') != 'menuitem')
  8922. 8922 : this.hide_menu(this.menu_stack[i], event);
  8923. 8923 : }
  8924. 8924 : if (stack && this.menu_stack.length) {
  8925. 8925 : obj.data('parent', $.last(this.menu_stack));
  8926. 8926 : obj.css('z-index', ($('#'+$.last(this.menu_stack)).css('z-index') || 0) + 1);
  8927. 8927 : }
  8928. 8928 : else if (!stack && this.menu_stack.length) {
  8929. 8929 : this.hide_menu(this.menu_stack[0], event);
  8930. 8930 : }
  8931. 8931 :
  8932. 8932 : obj.show().attr('aria-hidden', 'false').data('opener', ref.attr('aria-expanded', 'true').get(0));
  8933. 8933 : this.triggerEvent('menu-open', { name:name, obj:obj, props:prop, originalEvent:event });
  8934. 8934 : this.menu_stack.push(name);
  8935. 8935 :
  8936. 8936 : this.menu_keyboard_active = show && keyboard;
  8937. 8937 : if (this.menu_keyboard_active) {
  8938. 8938 : this.focused_menu = name;
  8939. 8939 : obj.find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus();
  8940. 8940 : }
  8941. 8941 : }
  8942. 8942 : else { // close menu
  8943. 8943 : this.hide_menu(name, event);
  8944. 8944 : }
  8945. 8945 :
  8946. 8946 : return show;
  8947. 8947 : };
  8948. 8948 :
  8949. 8949 : // hide the given popup menu (and its children)
  8950. 8950 : this.hide_menu = function(name, event)
  8951. 8951 : {
  8952. 8952 : if (!this.menu_stack.length) {
  8953. 8953 : // delegate to subscribers
  8954. 8954 : this.triggerEvent('menu-close', { name:name, props:{ menu:name }, originalEvent:event });
  8955. 8955 : return;
  8956. 8956 : }
  8957. 8957 :
  8958. 8958 : var obj, keyboard = rcube_event.is_keyboard(event);
  8959. 8959 : for (var j=this.menu_stack.length-1; j >= 0; j--) {
  8960. 8960 : obj = $('#' + this.menu_stack[j]).hide().attr('aria-hidden', 'true').data('parent', false);
  8961. 8961 : this.triggerEvent('menu-close', { name:this.menu_stack[j], obj:obj, props:{ menu:this.menu_stack[j] }, originalEvent:event });
  8962. 8962 : if (this.menu_stack[j] == name) {
  8963. 8963 : j = -1; // stop loop
  8964. 8964 : if (obj.data('opener')) {
  8965. 8965 : $(obj.data('opener')).attr('aria-expanded', 'false');
  8966. 8966 : if (keyboard)
  8967. 8967 : obj.data('opener').focus();
  8968. 8968 : }
  8969. 8969 : }
  8970. 8970 : this.menu_stack.pop();
  8971. 8971 : }
  8972. 8972 :
  8973. 8973 : // focus previous menu in stack
  8974. 8974 : if (this.menu_stack.length && keyboard) {
  8975. 8975 : this.menu_keyboard_active = true;
  8976. 8976 : this.focused_menu = $.last(this.menu_stack);
  8977. 8977 : if (!obj || !obj.data('opener'))
  8978. 8978 : $('#'+this.focused_menu).find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus();
  8979. 8979 : }
  8980. 8980 : else {
  8981. 8981 : this.focused_menu = null;
  8982. 8982 : this.menu_keyboard_active = false;
  8983. 8983 : }
  8984. 8984 : };
  8985. 8985 :
  8986. 8986 : // position a menu element on the screen in relation to other object
  8987. 8987 : this.element_position = function(element, obj)
  8988. 8988 : {
  8989. 8989 : var obj = $(obj), win = $(window),
  8990. 8990 : width = obj.outerWidth(),
  8991. 8991 : height = obj.outerHeight(),
  8992. 8992 : menu_pos = obj.data('menu-pos'),
  8993. 8993 : win_height = win.height(),
  8994. 8994 : elem_height = $(element).height(),
  8995. 8995 : elem_width = $(element).width(),
  8996. 8996 : pos = obj.offset(),
  8997. 8997 : top = pos.top,
  8998. 8998 : left = pos.left + width;
  8999. 8999 :
  9000. 9000 : if (menu_pos == 'bottom') {
  9001. 9001 : top += height;
  9002. 9002 : left -= width;
  9003. 9003 : }
  9004. 9004 : else
  9005. 9005 : left -= 5;
  9006. 9006 :
  9007. 9007 : if (top + elem_height > win_height) {
  9008. 9008 : top -= elem_height - height;
  9009. 9009 : if (top < 0)
  9010. 9010 : top = Math.max(0, (win_height - elem_height) / 2);
  9011. 9011 : }
  9012. 9012 :
  9013. 9013 : if (left + elem_width > win.width())
  9014. 9014 : left -= elem_width + width;
  9015. 9015 :
  9016. 9016 : element.css({left: left + 'px', top: top + 'px'});
  9017. 9017 : };
  9018. 9018 :
  9019. 9019 : // initialize HTML editor
  9020. 9020 : this.editor_init = function(config, id)
  9021. 9021 : {
  9022. 9022 : this.editor = new rcube_text_editor(config, id);
  9023. 9023 : };
  9024. 9024 :
  9025. 9025 :
  9026. 9026 : /********************************************************/
  9027. 9027 : /********* html to text conversion functions *********/
  9028. 9028 : /********************************************************/
  9029. 9029 :
  9030. 9030 : this.html2plain = function(html, func)
  9031. 9031 : {
  9032. 9032 : return this.format_converter(html, 'html', func);
  9033. 9033 : };
  9034. 9034 :
  9035. 9035 : this.plain2html = function(plain, func)
  9036. 9036 : {
  9037. 9037 : return this.format_converter(plain, 'plain', func);
  9038. 9038 : };
  9039. 9039 :
  9040. 9040 : this.format_converter = function(text, format, func)
  9041. 9041 : {
  9042. 9042 : // warn the user (if converted content is not empty)
  9043. 9043 : if (!text
  9044. 9044 : || (format == 'html' && !(text.replace(/<[^>]+>|&nbsp;|\xC2\xA0|\s/g, '')).length)
  9045. 9045 : || (format != 'html' && !(text.replace(/\xC2\xA0|\s/g, '')).length)
  9046. 9046 : ) {
  9047. 9047 : // without setTimeout() here, textarea is filled with initial (onload) content
  9048. 9048 : if (func)
  9049. 9049 : setTimeout(function() { func(''); }, 50);
  9050. 9050 : return true;
  9051. 9051 : }
  9052. 9052 :
  9053. 9053 : var confirmed = this.env.editor_warned || confirm(this.get_label('editorwarning'));
  9054. 9054 :
  9055. 9055 : this.env.editor_warned = true;
  9056. 9056 :
  9057. 9057 : if (!confirmed)
  9058. 9058 : return false;
  9059. 9059 :
  9060. 9060 : var url = '?_task=utils&_action=' + (format == 'html' ? 'html2text' : 'text2html'),
  9061. 9061 : lock = this.set_busy(true, 'converting');
  9062. 9062 :
  9063. 9063 : $.ajax({ type: 'POST', url: url, data: text, contentType: 'application/octet-stream',
  9064. 9064 : error: function(o, status, err) { ref.http_error(o, status, err, lock); },
  9065. 9065 : success: function(data) {
  9066. 9066 : ref.set_busy(false, null, lock);
  9067. 9067 : if (func) func(data);
  9068. 9068 : }
  9069. 9069 : });
  9070. 9070 :
  9071. 9071 : return true;
  9072. 9072 : };
  9073. 9073 :
  9074. 9074 :
  9075. 9075 : /********************************************************/
  9076. 9076 : /********* remote request methods *********/
  9077. 9077 : /********************************************************/
  9078. 9078 :
  9079. 9079 : // compose a valid url with the given parameters
  9080. 9080 : this.url = function(action, query)
  9081. 9081 : {
  9082. 9082 : var querystring = typeof query === 'string' ? query : '';
  9083. 9083 :
  9084. 9084 : if (typeof action !== 'string')
  9085. 9085 : query = action;
  9086. 9086 : else if (!query || typeof query !== 'object')
  9087. 9087 : query = {};
  9088. 9088 :
  9089. 9089 : if (action)
  9090. 9090 : query._action = action;
  9091. 9091 : else if (this.env.action)
  9092. 9092 : query._action = this.env.action;
  9093. 9093 :
  9094. 9094 : var url = this.env.comm_path, k, param = {};
  9095. 9095 :
  9096. 9096 : // overwrite task name
  9097. 9097 : if (action && action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) {
  9098. 9098 : query._action = RegExp.$2;
  9099. 9099 : url = url.replace(/\_task=[a-z0-9_-]+/, '_task=' + RegExp.$1);
  9100. 9100 : }
  9101. 9101 :
  9102. 9102 : // force _framed=0
  9103. 9103 : if (query._framed === 0) {
  9104. 9104 : url = url.replace('&_framed=1', '');
  9105. 9105 : query._framed = null;
  9106. 9106 : }
  9107. 9107 :
  9108. 9108 : // remove undefined values
  9109. 9109 : for (k in query) {
  9110. 9110 : if (query[k] !== undefined && query[k] !== null)
  9111. 9111 : param[k] = query[k];
  9112. 9112 : }
  9113. 9113 :
  9114. 9114 : if (param = $.param(param))
  9115. 9115 : url += (url.indexOf('?') > -1 ? '&' : '?') + param;
  9116. 9116 :
  9117. 9117 : if (querystring)
  9118. 9118 : url += (url.indexOf('?') > -1 ? '&' : '?') + querystring;
  9119. 9119 :
  9120. 9120 : return url;
  9121. 9121 : };
  9122. 9122 :
  9123. 9123 : this.redirect = function(url, lock)
  9124. 9124 : {
  9125. 9125 : if (lock !== false)
  9126. 9126 : this.set_busy(true, 'loading');
  9127. 9127 :
  9128. 9128 : if (this.is_framed()) {
  9129. 9129 : url = url.replace(/&_framed=1/, '');
  9130. 9130 : parent.rcmail.redirect(url, lock);
  9131. 9131 : }
  9132. 9132 : else {
  9133. 9133 : if (this.env.extwin) {
  9134. 9134 : if (typeof url == 'string')
  9135. 9135 : url += (url.indexOf('?') < 0 ? '?' : '&') + '_extwin=1';
  9136. 9136 : else
  9137. 9137 : url._extwin = 1;
  9138. 9138 : }
  9139. 9139 : this.location_href(url, window);
  9140. 9140 : }
  9141. 9141 : };
  9142. 9142 :
  9143. 9143 : this.goto_url = function(action, query, lock, secure)
  9144. 9144 : {
  9145. 9145 : var url = this.url(action, query)
  9146. 9146 : if (secure) url = this.secure_url(url);
  9147. 9147 : this.redirect(url, lock);
  9148. 9148 : };
  9149. 9149 :
  9150. 9150 : this.location_href = function(url, target, frame)
  9151. 9151 : {
  9152. 9152 : if (frame)
  9153. 9153 : this.lock_frame(target);
  9154. 9154 :
  9155. 9155 : if (typeof url == 'object')
  9156. 9156 : url = this.env.comm_path + '&' + $.param(url);
  9157. 9157 :
  9158. 9158 : // simulate real link click to force IE to send referer header
  9159. 9159 : if (bw.ie && target == window)
  9160. 9160 : $('<a>').attr('href', url).appendTo(document.body).get(0).click();
  9161. 9161 : else
  9162. 9162 : target.location.href = url;
  9163. 9163 :
  9164. 9164 : // reset keep-alive interval
  9165. 9165 : this.start_keepalive();
  9166. 9166 : };
  9167. 9167 :
  9168. 9168 : // update browser location to remember current view
  9169. 9169 : this.update_state = function(query)
  9170. 9170 : {
  9171. 9171 : if (window.history.replaceState)
  9172. 9172 : try {
  9173. 9173 : // This may throw security exception in Firefox (#5400)
  9174. 9174 : window.history.replaceState({}, document.title, rcmail.url('', query));
  9175. 9175 : }
  9176. 9176 : catch(e) { /* ignore */ };
  9177. 9177 : };
  9178. 9178 :
  9179. 9179 : // send a http request to the server
  9180. 9180 : this.http_request = function(action, data, lock, type)
  9181. 9181 : {
  9182. 9182 : if (type != 'POST')
  9183. 9183 : type = 'GET';
  9184. 9184 :
  9185. 9185 : if (typeof data !== 'object')
  9186. 9186 : data = rcube_parse_query(data);
  9187. 9187 :
  9188. 9188 : data._remote = 1;
  9189. 9189 : data._unlock = lock ? lock : 0;
  9190. 9190 :
  9191. 9191 : // trigger plugin hook
  9192. 9192 : var result = this.triggerEvent('request' + action, data);
  9193. 9193 :
  9194. 9194 : // abort if one of the handlers returned false
  9195. 9195 : if (result === false) {
  9196. 9196 : if (data._unlock)
  9197. 9197 : this.set_busy(false, null, data._unlock);
  9198. 9198 : return false;
  9199. 9199 : }
  9200. 9200 : else if (result && result.getResponseHeader) {
  9201. 9201 : return result;
  9202. 9202 : }
  9203. 9203 : else if (result !== undefined) {
  9204. 9204 : data = result;
  9205. 9205 : if (data._action) {
  9206. 9206 : action = data._action;
  9207. 9207 : delete data._action;
  9208. 9208 : }
  9209. 9209 : }
  9210. 9210 :
  9211. 9211 : var url = this.url(action);
  9212. 9212 :
  9213. 9213 : // reset keep-alive interval
  9214. 9214 : this.start_keepalive();
  9215. 9215 :
  9216. 9216 : // send request
  9217. 9217 : return $.ajax({
  9218. 9218 : type: type, url: url, data: data, dataType: 'json',
  9219. 9219 : success: function(data) { ref.http_response(data); },
  9220. 9220 : error: function(o, status, err) { ref.http_error(o, status, err, lock, action); }
  9221. 9221 : });
  9222. 9222 : };
  9223. 9223 :
  9224. 9224 : // send a http GET request to the server
  9225. 9225 : this.http_get = this.http_request;
  9226. 9226 :
  9227. 9227 : // send a http POST request to the server
  9228. 9228 : this.http_post = function(action, data, lock)
  9229. 9229 : {
  9230. 9230 : return this.http_request(action, data, lock, 'POST');
  9231. 9231 : };
  9232. 9232 :
  9233. 9233 : // aborts ajax request
  9234. 9234 : this.abort_request = function(r)
  9235. 9235 : {
  9236. 9236 : if (r.request)
  9237. 9237 : r.request.abort();
  9238. 9238 : if (r.lock)
  9239. 9239 : this.set_busy(false, null, r.lock);
  9240. 9240 : };
  9241. 9241 :
  9242. 9242 : // handle HTTP response
  9243. 9243 : this.http_response = function(response)
  9244. 9244 : {
  9245. 9245 : if (!response)
  9246. 9246 : return;
  9247. 9247 :
  9248. 9248 : if (response.unlock)
  9249. 9249 : this.set_busy(false, null, response.unlock);
  9250. 9250 :
  9251. 9251 : this.triggerEvent('responsebefore', {response: response});
  9252. 9252 : this.triggerEvent('responsebefore'+response.action, {response: response});
  9253. 9253 :
  9254. 9254 : // set env vars
  9255. 9255 : if (response.env)
  9256. 9256 : this.set_env(response.env);
  9257. 9257 :
  9258. 9258 : var i;
  9259. 9259 :
  9260. 9260 : // we have labels to add
  9261. 9261 : if (typeof response.texts === 'object') {
  9262. 9262 : for (i in response.texts)
  9263. 9263 : if (typeof response.texts[i] === 'string')
  9264. 9264 : this.add_label(i, response.texts[i]);
  9265. 9265 : }
  9266. 9266 :
  9267. 9267 : // if we get javascript code from server -> execute it
  9268. 9268 : if (response.exec) {
  9269. 9269 : eval(response.exec);
  9270. 9270 : }
  9271. 9271 :
  9272. 9272 : // execute callback functions of plugins
  9273. 9273 : if (response.callbacks && response.callbacks.length) {
  9274. 9274 : for (i=0; i < response.callbacks.length; i++)
  9275. 9275 : this.triggerEvent(response.callbacks[i][0], response.callbacks[i][1]);
  9276. 9276 : }
  9277. 9277 :
  9278. 9278 : // process the response data according to the sent action
  9279. 9279 : switch (response.action) {
  9280. 9280 : case 'mark':
  9281. 9281 : // Mark the message as Seen also in the opener/parent
  9282. 9282 : if ((this.env.action == 'show' || this.env.action == 'preview') && this.env.last_flag == 'SEEN')
  9283. 9283 : this.set_unread_message(this.env.uid, this.env.mailbox);
  9284. 9284 : break;
  9285. 9285 :
  9286. 9286 : case 'delete':
  9287. 9287 : if (this.task == 'addressbook') {
  9288. 9288 : var sid, uid = this.contact_list.get_selection(), writable = false;
  9289. 9289 :
  9290. 9290 : if (uid && this.contact_list.rows[uid]) {
  9291. 9291 : // search results, get source ID from record ID
  9292. 9292 : if (this.env.source == '') {
  9293. 9293 : sid = String(uid).replace(/^[^-]+-/, '');
  9294. 9294 : writable = sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly;
  9295. 9295 : }
  9296. 9296 : else {
  9297. 9297 : writable = !this.env.address_sources[this.env.source].readonly;
  9298. 9298 : }
  9299. 9299 : }
  9300. 9300 : this.enable_command('delete', 'edit', writable);
  9301. 9301 : this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
  9302. 9302 : this.enable_command('export-selected', 'print', false);
  9303. 9303 : }
  9304. 9304 :
  9305. 9305 : case 'move':
  9306. 9306 : if (this.env.action == 'show') {
  9307. 9307 : // re-enable commands on move/delete error
  9308. 9308 : this.enable_command(this.env.message_commands, true);
  9309. 9309 : if (!this.env.list_post)
  9310. 9310 : this.enable_command('reply-list', false);
  9311. 9311 : }
  9312. 9312 : else if (this.task == 'addressbook') {
  9313. 9313 : this.triggerEvent('listupdate', { list:this.contact_list, folder:this.env.source, rowcount:this.contact_list.rowcount });
  9314. 9314 : }
  9315. 9315 :
  9316. 9316 : case 'purge':
  9317. 9317 : case 'expunge':
  9318. 9318 : if (this.task == 'mail') {
  9319. 9319 : if (!this.env.exists) {
  9320. 9320 : // clear preview pane content
  9321. 9321 : if (this.env.contentframe)
  9322. 9322 : this.show_contentframe(false);
  9323. 9323 : // disable commands useless when mailbox is empty
  9324. 9324 : this.enable_command(this.env.message_commands, 'purge', 'expunge',
  9325. 9325 : 'select-all', 'select-none', 'expand-all', 'expand-unread', 'collapse-all', false);
  9326. 9326 : }
  9327. 9327 : if (this.message_list)
  9328. 9328 : this.triggerEvent('listupdate', { list:this.message_list, folder:this.env.mailbox, rowcount:this.message_list.rowcount });
  9329. 9329 : }
  9330. 9330 : break;
  9331. 9331 :
  9332. 9332 : case 'refresh':
  9333. 9333 : case 'check-recent':
  9334. 9334 : // update message flags
  9335. 9335 : $.each(this.env.recent_flags || {}, function(uid, flags) {
  9336. 9336 : ref.set_message(uid, 'deleted', flags.deleted);
  9337. 9337 : ref.set_message(uid, 'replied', flags.answered);
  9338. 9338 : ref.set_message(uid, 'unread', !flags.seen);
  9339. 9339 : ref.set_message(uid, 'forwarded', flags.forwarded);
  9340. 9340 : ref.set_message(uid, 'flagged', flags.flagged);
  9341. 9341 : });
  9342. 9342 : delete this.env.recent_flags;
  9343. 9343 :
  9344. 9344 : case 'getunread':
  9345. 9345 : case 'search':
  9346. 9346 : this.env.qsearch = null;
  9347. 9347 : case 'list':
  9348. 9348 : if (this.task == 'mail') {
  9349. 9349 : var is_multifolder = this.is_multifolder_listing(),
  9350. 9350 : list = this.message_list,
  9351. 9351 : uid = this.env.list_uid;
  9352. 9352 :
  9353. 9353 : this.enable_command('show', 'select-all', 'select-none', this.env.messagecount > 0);
  9354. 9354 : this.enable_command('expunge', 'purge', this.env.exists && !is_multifolder);
  9355. 9355 : this.enable_command('import-messages', !is_multifolder);
  9356. 9356 : this.enable_command('expand-all', 'expand-unread', 'collapse-all', this.env.threading && this.env.messagecount && !is_multifolder);
  9357. 9357 :
  9358. 9358 : if (list) {
  9359. 9359 : if (response.action == 'list' || response.action == 'search') {
  9360. 9360 : // highlight message row when we're back from message page
  9361. 9361 : if (uid) {
  9362. 9362 : if (uid === 'FIRST') {
  9363. 9363 : uid = list.get_first_row();
  9364. 9364 : }
  9365. 9365 : else if (uid === 'LAST') {
  9366. 9366 : uid = list.get_last_row();
  9367. 9367 : }
  9368. 9368 : else if (!list.rows[uid]) {
  9369. 9369 : uid += '-' + this.env.mailbox;
  9370. 9370 : }
  9371. 9371 :
  9372. 9372 : if (uid && list.rows[uid]) {
  9373. 9373 : list.select(uid);
  9374. 9374 : }
  9375. 9375 :
  9376. 9376 : delete this.env.list_uid;
  9377. 9377 : }
  9378. 9378 :
  9379. 9379 : this.enable_command('set-listmode', this.env.threads && !is_multifolder);
  9380. 9380 : if (list.rowcount > 0 && !$(document.activeElement).is('input,textarea'))
  9381. 9381 : list.focus();
  9382. 9382 :
  9383. 9383 : // trigger 'select' so all dependent actions update its state
  9384. 9384 : // e.g. plugins use this event to activate buttons (#1490647)
  9385. 9385 : list.triggerEvent('select');
  9386. 9386 : }
  9387. 9387 :
  9388. 9388 : if (response.action != 'getunread')
  9389. 9389 : this.triggerEvent('listupdate', { list:list, folder:this.env.mailbox, rowcount:list.rowcount });
  9390. 9390 : }
  9391. 9391 : }
  9392. 9392 : else if (this.task == 'addressbook') {
  9393. 9393 : var list = this.contact_list,
  9394. 9394 : uid = this.env.list_uid;
  9395. 9395 :
  9396. 9396 : this.enable_command('export', 'select-all', 'select-none', (list && list.rowcount > 0));
  9397. 9397 :
  9398. 9398 : if (response.action == 'list' || response.action == 'search') {
  9399. 9399 : // PAMELA - Search contacts by source
  9400. 9400 : this.enable_command('search-create', response.action == 'search');
  9401. 9401 : this.enable_command('search-delete', this.env.search_id);
  9402. 9402 : this.update_group_commands();
  9403. 9403 :
  9404. 9404 : if (list && uid) {
  9405. 9405 : if (uid === 'FIRST') {
  9406. 9406 : uid = list.get_first_row();
  9407. 9407 : }
  9408. 9408 : else if (uid === 'LAST') {
  9409. 9409 : uid = list.get_last_row();
  9410. 9410 : }
  9411. 9411 :
  9412. 9412 : if (uid && list.rows[uid]) {
  9413. 9413 : list.select(uid);
  9414. 9414 : }
  9415. 9415 :
  9416. 9416 : delete this.env.list_uid;
  9417. 9417 :
  9418. 9418 : // trigger 'select' so all dependent actions update its state
  9419. 9419 : list.triggerEvent('select');
  9420. 9420 : }
  9421. 9421 :
  9422. 9422 : if (list.rowcount > 0 && !$(document.activeElement).is('input,textarea'))
  9423. 9423 : list.focus();
  9424. 9424 :
  9425. 9425 : this.triggerEvent('listupdate', { list:list, folder:this.env.source, rowcount:list.rowcount });
  9426. 9426 : }
  9427. 9427 : }
  9428. 9428 : break;
  9429. 9429 :
  9430. 9430 : case 'list-contacts':
  9431. 9431 : case 'search-contacts':
  9432. 9432 : if (this.contact_list) {
  9433. 9433 : if (this.contact_list.rowcount > 0)
  9434. 9434 : this.contact_list.focus();
  9435. 9435 : this.triggerEvent('listupdate', { list:this.contact_list, rowcount:this.contact_list.rowcount });
  9436. 9436 : }
  9437. 9437 : break;
  9438. 9438 : }
  9439. 9439 :
  9440. 9440 : if (response.unlock)
  9441. 9441 : this.hide_message(response.unlock);
  9442. 9442 :
  9443. 9443 : this.triggerEvent('responseafter', {response: response});
  9444. 9444 : this.triggerEvent('responseafter'+response.action, {response: response});
  9445. 9445 :
  9446. 9446 : // reset keep-alive interval
  9447. 9447 : this.start_keepalive();
  9448. 9448 : };
  9449. 9449 :
  9450. 9450 : // handle HTTP request errors
  9451. 9451 : this.http_error = function(request, status, err, lock, action)
  9452. 9452 : {
  9453. 9453 : var errmsg = request.statusText;
  9454. 9454 :
  9455. 9455 : this.set_busy(false, null, lock);
  9456. 9456 : request.abort();
  9457. 9457 :
  9458. 9458 : // don't display error message on page unload (#1488547)
  9459. 9459 : if (this.unload)
  9460. 9460 : return;
  9461. 9461 :
  9462. 9462 : if (request.status && errmsg)
  9463. 9463 : this.display_message(this.get_label('servererror') + ' (' + errmsg + ')', 'error');
  9464. 9464 : else if (status == 'timeout')
  9465. 9465 : this.display_message('requesttimedout', 'error');
  9466. 9466 : else if (request.status == 0 && status != 'abort')
  9467. 9467 : this.display_message('connerror', 'error');
  9468. 9468 :
  9469. 9469 : // redirect to url specified in location header if not empty
  9470. 9470 : var location_url = request.getResponseHeader("Location");
  9471. 9471 : if (location_url && this.env.action != 'compose') // don't redirect on compose screen, contents might get lost (#1488926)
  9472. 9472 : this.redirect(location_url);
  9473. 9473 :
  9474. 9474 : // 403 Forbidden response (CSRF prevention) - reload the page.
  9475. 9475 : // In case there's a new valid session it will be used, otherwise
  9476. 9476 : // login form will be presented (#1488960).
  9477. 9477 : if (request.status == 403) {
  9478. 9478 : (this.is_framed() ? parent : window).location.reload();
  9479. 9479 : return;
  9480. 9480 : }
  9481. 9481 :
  9482. 9482 : // re-send keep-alive requests after 30 seconds
  9483. 9483 : if (action == 'keep-alive')
  9484. 9484 : setTimeout(function() { ref.keep_alive(); ref.start_keepalive(); }, 30000);
  9485. 9485 : };
  9486. 9486 :
  9487. 9487 : // handler for session errors detected on the server
  9488. 9488 : this.session_error = function(redirect_url)
  9489. 9489 : {
  9490. 9490 : this.env.server_error = 401;
  9491. 9491 :
  9492. 9492 : // save message in local storage and do not redirect
  9493. 9493 : if (this.env.action == 'compose') {
  9494. 9494 : this.save_compose_form_local();
  9495. 9495 : this.compose_skip_unsavedcheck = true;
  9496. 9496 : // stop keep-alive and refresh processes
  9497. 9497 : this.env.session_lifetime = 0;
  9498. 9498 : if (this._keepalive)
  9499. 9499 : clearInterval(this._keepalive);
  9500. 9500 : if (this._refresh)
  9501. 9501 : clearInterval(this._refresh);
  9502. 9502 : }
  9503. 9503 : else if (redirect_url) {
  9504. 9504 : setTimeout(function() { ref.redirect(redirect_url, true); }, 2000);
  9505. 9505 : }
  9506. 9506 : };
  9507. 9507 :
  9508. 9508 : // callback when an iframe finished loading
  9509. 9509 : this.iframe_loaded = function(unlock)
  9510. 9510 : {
  9511. 9511 : if (!unlock)
  9512. 9512 : unlock = this.env.frame_lock;
  9513. 9513 :
  9514. 9514 : this.set_busy(false, null, unlock);
  9515. 9515 :
  9516. 9516 : if (this.submit_timer)
  9517. 9517 : clearTimeout(this.submit_timer);
  9518. 9518 : };
  9519. 9519 :
  9520. 9520 : /**
  9521. 9521 : Send multi-threaded parallel HTTP requests to the server for a list if items.
  9522. 9522 : The string '%' in either a GET query or POST parameters will be replaced with the respective item value.
  9523. 9523 : This is the argument object expected: {
  9524. 9524 : items: ['foo','bar','gna'], // list of items to send requests for
  9525. 9525 : action: 'task/some-action', // Roundcube action to call
  9526. 9526 : query: { q:'%s' }, // GET query parameters
  9527. 9527 : postdata: { source:'%s' }, // POST data (sends a POST request if present)
  9528. 9528 : threads: 3, // max. number of concurrent requests
  9529. 9529 : onresponse: function(data){ }, // Callback function called for every response received from server
  9530. 9530 : whendone: function(alldata){ } // Callback function called when all requests have been sent
  9531. 9531 : }
  9532. 9532 : */
  9533. 9533 : this.multi_thread_http_request = function(prop)
  9534. 9534 : {
  9535. 9535 : var i, item, reqid = new Date().getTime(),
  9536. 9536 : threads = prop.threads || 1;
  9537. 9537 :
  9538. 9538 : prop.reqid = reqid;
  9539. 9539 : prop.running = 0;
  9540. 9540 : prop.requests = [];
  9541. 9541 : prop.result = [];
  9542. 9542 : prop._items = $.extend([], prop.items); // copy items
  9543. 9543 :
  9544. 9544 : if (!prop.lock)
  9545. 9545 : prop.lock = this.display_message('', 'loading');
  9546. 9546 :
  9547. 9547 : // add the request arguments to the jobs pool
  9548. 9548 : this.http_request_jobs[reqid] = prop;
  9549. 9549 :
  9550. 9550 : // start n threads
  9551. 9551 : for (i=0; i < threads; i++) {
  9552. 9552 : item = prop._items.shift();
  9553. 9553 : if (item === undefined)
  9554. 9554 : break;
  9555. 9555 :
  9556. 9556 : prop.running++;
  9557. 9557 : prop.requests.push(this.multi_thread_send_request(prop, item));
  9558. 9558 : }
  9559. 9559 :
  9560. 9560 : return reqid;
  9561. 9561 : };
  9562. 9562 :
  9563. 9563 : // helper method to send an HTTP request with the given iterator value
  9564. 9564 : this.multi_thread_send_request = function(prop, item)
  9565. 9565 : {
  9566. 9566 : var k, postdata, query;
  9567. 9567 :
  9568. 9568 : // replace %s in post data
  9569. 9569 : if (prop.postdata) {
  9570. 9570 : postdata = {};
  9571. 9571 : for (k in prop.postdata) {
  9572. 9572 : postdata[k] = String(prop.postdata[k]).replace('%s', item);
  9573. 9573 : }
  9574. 9574 : postdata._reqid = prop.reqid;
  9575. 9575 : }
  9576. 9576 : // replace %s in query
  9577. 9577 : else if (typeof prop.query == 'string') {
  9578. 9578 : query = prop.query.replace('%s', item);
  9579. 9579 : query += '&_reqid=' + prop.reqid;
  9580. 9580 : }
  9581. 9581 : else if (typeof prop.query == 'object' && prop.query) {
  9582. 9582 : query = {};
  9583. 9583 : for (k in prop.query) {
  9584. 9584 : query[k] = String(prop.query[k]).replace('%s', item);
  9585. 9585 : }
  9586. 9586 : query._reqid = prop.reqid;
  9587. 9587 : }
  9588. 9588 :
  9589. 9589 : // send HTTP GET or POST request
  9590. 9590 : return postdata ? this.http_post(prop.action, postdata) : this.http_request(prop.action, query);
  9591. 9591 : };
  9592. 9592 :
  9593. 9593 : // callback function for multi-threaded http responses
  9594. 9594 : this.multi_thread_http_response = function(data, reqid)
  9595. 9595 : {
  9596. 9596 : var prop = this.http_request_jobs[reqid];
  9597. 9597 : if (!prop || prop.running <= 0 || prop.cancelled)
  9598. 9598 : return;
  9599. 9599 :
  9600. 9600 : prop.running--;
  9601. 9601 :
  9602. 9602 : // trigger response callback
  9603. 9603 : if (prop.onresponse && typeof prop.onresponse == 'function') {
  9604. 9604 : prop.onresponse(data);
  9605. 9605 : }
  9606. 9606 :
  9607. 9607 : prop.result = $.extend(prop.result, data);
  9608. 9608 :
  9609. 9609 : // send next request if prop.items is not yet empty
  9610. 9610 : var item = prop._items.shift();
  9611. 9611 : if (item !== undefined) {
  9612. 9612 : prop.running++;
  9613. 9613 : prop.requests.push(this.multi_thread_send_request(prop, item));
  9614. 9614 : }
  9615. 9615 : // trigger whendone callback and mark this request as done
  9616. 9616 : else if (prop.running == 0) {
  9617. 9617 : if (prop.whendone && typeof prop.whendone == 'function') {
  9618. 9618 : prop.whendone(prop.result);
  9619. 9619 : }
  9620. 9620 :
  9621. 9621 : this.set_busy(false, '', prop.lock);
  9622. 9622 :
  9623. 9623 : // remove from this.http_request_jobs pool
  9624. 9624 : delete this.http_request_jobs[reqid];
  9625. 9625 : }
  9626. 9626 : };
  9627. 9627 :
  9628. 9628 : // abort a running multi-thread request with the given identifier
  9629. 9629 : this.multi_thread_request_abort = function(reqid)
  9630. 9630 : {
  9631. 9631 : var prop = this.http_request_jobs[reqid];
  9632. 9632 : if (prop) {
  9633. 9633 : for (var i=0; prop.running > 0 && i < prop.requests.length; i++) {
  9634. 9634 : if (prop.requests[i].abort)
  9635. 9635 : prop.requests[i].abort();
  9636. 9636 : }
  9637. 9637 :
  9638. 9638 : prop.running = 0;
  9639. 9639 : prop.cancelled = true;
  9640. 9640 : this.set_busy(false, '', prop.lock);
  9641. 9641 : }
  9642. 9642 : };
  9643. 9643 :
  9644. 9644 : // post the given form to a hidden iframe
  9645. 9645 : this.async_upload_form = function(form, action, onload)
  9646. 9646 : {
  9647. 9647 : // create hidden iframe
  9648. 9648 : var ts = new Date().getTime(),
  9649. 9649 : frame_name = 'rcmupload' + ts,
  9650. 9650 : frame = this.dummy_iframe(frame_name);
  9651. 9651 :
  9652. 9652 : // handle upload errors by parsing iframe content in onload
  9653. 9653 : frame.on('load', {ts:ts}, onload);
  9654. 9654 :
  9655. 9655 : $(form).attr({
  9656. 9656 : target: frame_name,
  9657. 9657 : action: this.url(action, {_id: this.env.compose_id || '', _uploadid: ts, _from: this.env.action}),
  9658. 9658 : method: 'POST',
  9659. 9659 : enctype: 'multipart/form-data'
  9660. 9660 : })
  9661. 9661 : .submit();
  9662. 9662 :
  9663. 9663 : return frame_name;
  9664. 9664 : };
  9665. 9665 :
  9666. 9666 : // create hidden iframe element
  9667. 9667 : this.dummy_iframe = function(name, src)
  9668. 9668 : {
  9669. 9669 : return $('<iframe>').attr({
  9670. 9670 : name: name,
  9671. 9671 : src: src,
  9672. 9672 : style: 'width:0;height:0;visibility:hidden',
  9673. 9673 : 'aria-hidden': 'true'
  9674. 9674 : })
  9675. 9675 : .appendTo(document.body);
  9676. 9676 : };
  9677. 9677 :
  9678. 9678 : // html5 file-drop API
  9679. 9679 : this.document_drag_hover = function(e, over)
  9680. 9680 : {
  9681. 9681 : // don't e.preventDefault() here to not block text dragging on the page (#1490619)
  9682. 9682 : $(this.gui_objects.filedrop)[(over?'addClass':'removeClass')]('active');
  9683. 9683 : };
  9684. 9684 :
  9685. 9685 : this.file_drag_hover = function(e, over)
  9686. 9686 : {
  9687. 9687 : e.preventDefault();
  9688. 9688 : e.stopPropagation();
  9689. 9689 : $(this.gui_objects.filedrop)[(over?'addClass':'removeClass')]('hover');
  9690. 9690 : };
  9691. 9691 :
  9692. 9692 : // handler when files are dropped to a designated area.
  9693. 9693 : // compose a multipart form data and submit it to the server
  9694. 9694 : this.file_dropped = function(e)
  9695. 9695 : {
  9696. 9696 : // abort event and reset UI
  9697. 9697 : this.file_drag_hover(e, false);
  9698. 9698 :
  9699. 9699 : // prepare multipart form data composition
  9700. 9700 : var uri,
  9701. 9701 : files = e.target.files || e.dataTransfer.files,
  9702. 9702 : args = {_id: this.env.compose_id || this.env.cid || '', _remote: 1, _from: this.env.action};
  9703. 9703 :
  9704. 9704 : if (!files || !files.length) {
  9705. 9705 : // Roundcube attachment, pass its uri to the backend and attach
  9706. 9706 : if (uri = e.dataTransfer.getData('roundcube-uri')) {
  9707. 9707 : var ts = 'upload' + new Date().getTime(),
  9708. 9708 : // jQuery way to escape filename (#1490530)
  9709. 9709 : content = $('<span>').text(e.dataTransfer.getData('roundcube-name') || this.get_label('attaching')).html();
  9710. 9710 :
  9711. 9711 : args._uri = uri;
  9712. 9712 : args._uploadid = ts;
  9713. 9713 :
  9714. 9714 : // add to attachments list
  9715. 9715 : if (!this.add2attachment_list(ts, {name: '', html: content, classname: 'uploading', complete: false}))
  9716. 9716 : this.file_upload_id = this.set_busy(true, 'attaching');
  9717. 9717 :
  9718. 9718 : this.http_post(this.env.filedrop.action || 'upload', args);
  9719. 9719 : }
  9720. 9720 :
  9721. 9721 : return;
  9722. 9722 : }
  9723. 9723 :
  9724. 9724 : this.file_upload(files, args, {
  9725. 9725 : name: (this.env.filedrop.fieldname || '_file') + (this.env.filedrop.single ? '' : '[]'),
  9726. 9726 : single: this.env.filedrop.single,
  9727. 9727 : filter: this.env.filedrop.filter,
  9728. 9728 : action: ref.env.filedrop.action
  9729. 9729 : });
  9730. 9730 : };
  9731. 9731 :
  9732. 9732 : // Files upload using ajax
  9733. 9733 : this.file_upload = function(files, post_args, props)
  9734. 9734 : {
  9735. 9735 : if (!window.FormData || !files || !files.length)
  9736. 9736 : return false;
  9737. 9737 :
  9738. 9738 : var f, i, fname, size = 0, numfiles = 0,
  9739. 9739 : formdata = new FormData(),
  9740. 9740 : fieldname = props.name || '_file[]',
  9741. 9741 : limit = props.single ? 1 : files.length;
  9742. 9742 : args = $.extend({_remote: 1, _from: this.env.action}, post_args || {});
  9743. 9743 :
  9744. 9744 : // add files to form data
  9745. 9745 : for (i=0; numfiles < limit && (f = files[i]); i++) {
  9746. 9746 : // filter by file type if requested
  9747. 9747 : if (props.filter && !f.type.match(new RegExp(props.filter))) {
  9748. 9748 : // TODO: show message to user
  9749. 9749 : continue;
  9750. 9750 : }
  9751. 9751 :
  9752. 9752 : formdata.append(fieldname, f);
  9753. 9753 : size += f.size;
  9754. 9754 : fname = f.name;
  9755. 9755 : numfiles++;
  9756. 9756 : }
  9757. 9757 :
  9758. 9758 : if (numfiles) {
  9759. 9759 : if (this.env.max_filesize && this.env.filesizeerror && size > this.env.max_filesize) {
  9760. 9760 : this.display_message(this.env.filesizeerror, 'error');
  9761. 9761 : return false;
  9762. 9762 : }
  9763. 9763 :
  9764. 9764 : if (this.env.max_filecount && this.env.filecounterror && numfiles > this.env.max_filecount) {
  9765. 9765 : this.display_message(this.env.filecounterror, 'error');
  9766. 9766 : return false;
  9767. 9767 : }
  9768. 9768 :
  9769. 9769 : var ts = 'upload' + new Date().getTime(),
  9770. 9770 : label = numfiles > 1 ? this.get_label('uploadingmany') : fname,
  9771. 9771 : // jQuery way to escape filename (#1490530)
  9772. 9772 : content = $('<span>').text(label).html();
  9773. 9773 :
  9774. 9774 : // add to attachments list
  9775. 9775 : if (!this.add2attachment_list(ts, {name: '', html: content, classname: 'uploading', complete: false}) && !props.lock)
  9776. 9776 : props.lock = this.file_upload_id = this.set_busy(true, 'uploading');
  9777. 9777 :
  9778. 9778 : args._uploadid = ts;
  9779. 9779 : args._unlock = props.lock;
  9780. 9780 :
  9781. 9781 : this.uploads[ts] = $.ajax({
  9782. 9782 : type: 'POST',
  9783. 9783 : dataType: 'json',
  9784. 9784 : url: this.url(props.action || 'upload', args),
  9785. 9785 : contentType: false,
  9786. 9786 : processData: false,
  9787. 9787 : timeout: this.uploadTimeout, // ajax call timeout for loading attachment
  9788. 9788 : data: formdata,
  9789. 9789 : headers: {'X-Roundcube-Request': this.env.request_token},
  9790. 9790 : xhr: function() {
  9791. 9791 : var xhr = $.ajaxSettings.xhr();
  9792. 9792 : if (xhr.upload && ref.labels.uploadprogress) {
  9793. 9793 : xhr.upload.onprogress = function(e) {
  9794. 9794 : var msg = ref.file_upload_msg(e.loaded, e.total);
  9795. 9795 : if (msg) {
  9796. 9796 : $('#' + ts).find('.uploading').text(msg);
  9797. 9797 : }
  9798. 9798 : };
  9799. 9799 : }
  9800. 9800 : return xhr;
  9801. 9801 : },
  9802. 9802 : success: function(data) {
  9803. 9803 : delete ref.uploads[ts];
  9804. 9804 : ref.http_response(data);
  9805. 9805 : },
  9806. 9806 : error: function(o, status, err) {
  9807. 9807 : delete ref.uploads[ts];
  9808. 9808 : ref.remove_from_attachment_list(ts);
  9809. 9809 : ref.http_error(o, status, err, props.lock, 'attachment');
  9810. 9810 : }
  9811. 9811 : });
  9812. 9812 : }
  9813. 9813 :
  9814. 9814 : return true;
  9815. 9815 : };
  9816. 9816 :
  9817. 9817 : this.file_upload_msg = function(current, total)
  9818. 9818 : {
  9819. 9819 : if (total && current < total) {
  9820. 9820 : var percent = Math.round(current/total * 100),
  9821. 9821 : label = ref.get_label('uploadprogress');
  9822. 9822 :
  9823. 9823 : if (total >= 1073741824) {
  9824. 9824 : total = parseFloat(total/1073741824).toFixed(1) + ' ' . this.get_label('GB');
  9825. 9825 : current = parseFloat(current/1073741824).toFixed(1);
  9826. 9826 : }
  9827. 9827 : else if (total >= 1048576) {
  9828. 9828 : total = parseFloat(total/1048576).toFixed(1) + ' ' + this.get_label('MB');
  9829. 9829 : current = parseFloat(current/1048576).toFixed(1);
  9830. 9830 : }
  9831. 9831 : else if (total >= 1024) {
  9832. 9832 : total = parseInt(total/1024) + ' ' + this.get_label('KB');
  9833. 9833 : current = parseInt(current/1024);
  9834. 9834 : }
  9835. 9835 : else {
  9836. 9836 : total = total + ' ' + this.get_label('B');
  9837. 9837 : }
  9838. 9838 :
  9839. 9839 : return label.replace('$percent', percent + '%').replace('$current', current).replace('$total', total);
  9840. 9840 : }
  9841. 9841 : };
  9842. 9842 :
  9843. 9843 : // starts interval for keep-alive signal
  9844. 9844 : this.start_keepalive = function()
  9845. 9845 : {
  9846. 9846 : if (!this.env.session_lifetime || this.env.framed || this.env.extwin || this.task == 'login' || this.env.action == 'print')
  9847. 9847 : return;
  9848. 9848 :
  9849. 9849 : if (this._keepalive)
  9850. 9850 : clearInterval(this._keepalive);
  9851. 9851 :
  9852. 9852 : // use Math to prevent from an integer overflow (#5273)
  9853. 9853 : // maximum interval is 15 minutes, minimum is 30 seconds
  9854. 9854 : var interval = Math.min(1800, this.env.session_lifetime) * 0.5 * 1000;
  9855. 9855 : this._keepalive = setInterval(function() { ref.keep_alive(); }, interval < 30000 ? 30000 : interval);
  9856. 9856 : };
  9857. 9857 :
  9858. 9858 : // starts interval for refresh signal
  9859. 9859 : this.start_refresh = function()
  9860. 9860 : {
  9861. 9861 : if (!this.env.refresh_interval || this.env.framed || this.env.extwin || this.task == 'login' || this.env.action == 'print')
  9862. 9862 : return;
  9863. 9863 :
  9864. 9864 : if (this._refresh)
  9865. 9865 : clearInterval(this._refresh);
  9866. 9866 :
  9867. 9867 : this._refresh = setInterval(function(){ ref.refresh(); }, this.env.refresh_interval * 1000);
  9868. 9868 : };
  9869. 9869 :
  9870. 9870 : // sends keep-alive signal
  9871. 9871 : this.keep_alive = function()
  9872. 9872 : {
  9873. 9873 : if (!this.busy)
  9874. 9874 : this.http_request('keep-alive');
  9875. 9875 : };
  9876. 9876 :
  9877. 9877 : // sends refresh signal
  9878. 9878 : this.refresh = function()
  9879. 9879 : {
  9880. 9880 : if (this.busy) {
  9881. 9881 : // try again after 10 seconds
  9882. 9882 : setTimeout(function() { ref.refresh(); ref.start_refresh(); }, 10000);
  9883. 9883 : return;
  9884. 9884 : }
  9885. 9885 :
  9886. 9886 : var params = {}, lock = this.set_busy(true, 'refreshing');
  9887. 9887 :
  9888. 9888 : if (this.task == 'mail' && this.gui_objects.mailboxlist)
  9889. 9889 : params = this.check_recent_params();
  9890. 9890 :
  9891. 9891 : params._last = Math.floor(this.env.lastrefresh.getTime() / 1000);
  9892. 9892 : this.env.lastrefresh = new Date();
  9893. 9893 :
  9894. 9894 : // plugins should bind to 'requestrefresh' event to add own params
  9895. 9895 : this.http_post('refresh', params, lock);
  9896. 9896 : };
  9897. 9897 :
  9898. 9898 : // returns check-recent request parameters
  9899. 9899 : this.check_recent_params = function()
  9900. 9900 : {
  9901. 9901 : var params = {_mbox: this.env.mailbox};
  9902. 9902 :
  9903. 9903 : if (this.gui_objects.mailboxlist)
  9904. 9904 : params._folderlist = 1;
  9905. 9905 : if (this.gui_objects.quotadisplay)
  9906. 9906 : params._quota = 1;
  9907. 9907 : if (this.env.search_request)
  9908. 9908 : params._search = this.env.search_request;
  9909. 9909 :
  9910. 9910 : if (this.gui_objects.messagelist) {
  9911. 9911 : params._list = 1;
  9912. 9912 :
  9913. 9913 : // message uids for flag updates check
  9914. 9914 : params._uids = $.map(this.message_list.rows, function(row, uid) { return uid; }).join(',');
  9915. 9915 : }
  9916. 9916 :
  9917. 9917 : return params;
  9918. 9918 : };
  9919. 9919 :
  9920. 9920 :
  9921. 9921 : /********************************************************/
  9922. 9922 : /********* helper methods *********/
  9923. 9923 : /********************************************************/
  9924. 9924 :
  9925. 9925 : /**
  9926. 9926 : * Quote html entities
  9927. 9927 : */
  9928. 9928 : this.quote_html = function(str)
  9929. 9929 : {
  9930. 9930 : return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  9931. 9931 : };
  9932. 9932 :
  9933. 9933 : // get window.opener.rcmail if available
  9934. 9934 : this.opener = function(deep, filter)
  9935. 9935 : {
  9936. 9936 : var i, win = window.opener;
  9937. 9937 :
  9938. 9938 : // catch Error: Permission denied to access property rcmail
  9939. 9939 : try {
  9940. 9940 : if (win && !win.closed && win !== window) {
  9941. 9941 : // try parent of the opener window, e.g. preview frame
  9942. 9942 : if (deep && (!win.rcmail || win.rcmail.env.framed) && win.parent && win.parent.rcmail)
  9943. 9943 : win = win.parent;
  9944. 9944 :
  9945. 9945 : if (win.rcmail && filter)
  9946. 9946 : for (i in filter)
  9947. 9947 : if (win.rcmail.env[i] != filter[i])
  9948. 9948 : return;
  9949. 9949 :
  9950. 9950 : return win.rcmail;
  9951. 9951 : }
  9952. 9952 : }
  9953. 9953 : catch (e) {}
  9954. 9954 : };
  9955. 9955 :
  9956. 9956 : // check if we're in show mode or if we have a unique selection
  9957. 9957 : // and return the message uid
  9958. 9958 : this.get_single_uid = function()
  9959. 9959 : {
  9960. 9960 : var uid = this.env.uid || (this.message_list ? this.message_list.get_single_selection() : null);
  9961. 9961 : var result = ref.triggerEvent('get_single_uid', { uid: uid });
  9962. 9962 : return result || uid;
  9963. 9963 : };
  9964. 9964 :
  9965. 9965 : // same as above but for contacts
  9966. 9966 : this.get_single_cid = function()
  9967. 9967 : {
  9968. 9968 : var cid = this.env.cid || (this.contact_list ? this.contact_list.get_single_selection() : null);
  9969. 9969 : var result = ref.triggerEvent('get_single_cid', { cid: cid });
  9970. 9970 : return result || cid;
  9971. 9971 : };
  9972. 9972 :
  9973. 9973 : // get the IMP mailbox of the message with the given UID
  9974. 9974 : this.get_message_mailbox = function(uid)
  9975. 9975 : {
  9976. 9976 : var msg;
  9977. 9977 :
  9978. 9978 : if (this.env.messages && uid && (msg = this.env.messages[uid]) && msg.mbox)
  9979. 9979 : return msg.mbox;
  9980. 9980 :
  9981. 9981 : if (/^[0-9]+-(.*)$/.test(uid))
  9982. 9982 : return RegExp.$1;
  9983. 9983 :
  9984. 9984 : return this.env.mailbox;
  9985. 9985 : };
  9986. 9986 :
  9987. 9987 : // build request parameters from single message id (maybe with mailbox name)
  9988. 9988 : this.params_from_uid = function(uid, params)
  9989. 9989 : {
  9990. 9990 : if (!params)
  9991. 9991 : params = {};
  9992. 9992 :
  9993. 9993 : params._uid = String(uid).split('-')[0];
  9994. 9994 : params._mbox = this.get_message_mailbox(uid);
  9995. 9995 :
  9996. 9996 : return params;
  9997. 9997 : };
  9998. 9998 :
  9999. 9999 : // gets cursor position
  10000. 10000 : this.get_caret_pos = function(obj)
  10001. 10001 : {
  10002. 10002 : if (obj.selectionEnd !== undefined)
  10003. 10003 : return obj.selectionEnd;
  10004. 10004 :
  10005. 10005 : return obj.value.length;
  10006. 10006 : };
  10007. 10007 :
  10008. 10008 : // moves cursor to specified position
  10009. 10009 : this.set_caret_pos = function(obj, pos)
  10010. 10010 : {
  10011. 10011 : try {
  10012. 10012 : if (obj.setSelectionRange)
  10013. 10013 : obj.setSelectionRange(pos, pos);
  10014. 10014 : }
  10015. 10015 : catch(e) {} // catch Firefox exception if obj is hidden
  10016. 10016 : };
  10017. 10017 :
  10018. 10018 : // get selected text from an input field
  10019. 10019 : this.get_input_selection = function(obj)
  10020. 10020 : {
  10021. 10021 : var start = 0, end = 0, normalizedValue = '';
  10022. 10022 :
  10023. 10023 : if (typeof obj.selectionStart == "number" && typeof obj.selectionEnd == "number") {
  10024. 10024 : normalizedValue = obj.value;
  10025. 10025 : start = obj.selectionStart;
  10026. 10026 : end = obj.selectionEnd;
  10027. 10027 : }
  10028. 10028 :
  10029. 10029 : return {start: start, end: end, text: normalizedValue.substr(start, end-start)};
  10030. 10030 : };
  10031. 10031 :
  10032. 10032 : // disable/enable all fields of a form
  10033. 10033 : this.lock_form = function(form, lock)
  10034. 10034 : {
  10035. 10035 : if (!form || !form.elements)
  10036. 10036 : return;
  10037. 10037 :
  10038. 10038 : if (lock)
  10039. 10039 : this.disabled_form_elements = [];
  10040. 10040 :
  10041. 10041 : $.each(form.elements, function() {
  10042. 10042 : if (this.type == 'hidden')
  10043. 10043 : return;
  10044. 10044 : // remember which elem was disabled before lock
  10045. 10045 : if (lock && this.disabled)
  10046. 10046 : ref.disabled_form_elements.push(this);
  10047. 10047 : else if (lock || $.inArray(this, ref.disabled_form_elements) < 0)
  10048. 10048 : this.disabled = lock;
  10049. 10049 : });
  10050. 10050 : };
  10051. 10051 :
  10052. 10052 : this.mailto_handler_uri = function()
  10053. 10053 : {
  10054. 10054 : return location.href.split('?')[0] + '?_task=mail&_action=compose&_to=%s';
  10055. 10055 : };
  10056. 10056 :
  10057. 10057 : this.register_protocol_handler = function(name)
  10058. 10058 : {
  10059. 10059 : try {
  10060. 10060 : window.navigator.registerProtocolHandler('mailto', this.mailto_handler_uri(), name);
  10061. 10061 : }
  10062. 10062 : catch(e) {
  10063. 10063 : this.display_message(String(e), 'error');
  10064. 10064 : }
  10065. 10065 : };
  10066. 10066 :
  10067. 10067 : this.check_protocol_handler = function(name, elem)
  10068. 10068 : {
  10069. 10069 : var nav = window.navigator;
  10070. 10070 :
  10071. 10071 : if (!nav || (typeof nav.registerProtocolHandler != 'function')) {
  10072. 10072 : $(elem).addClass('disabled').click(function() {
  10073. 10073 : ref.display_message('nosupporterror', 'error');
  10074. 10074 : return false;
  10075. 10075 : });
  10076. 10076 : }
  10077. 10077 : else if (typeof nav.isProtocolHandlerRegistered == 'function') {
  10078. 10078 : var status = nav.isProtocolHandlerRegistered('mailto', this.mailto_handler_uri());
  10079. 10079 : if (status)
  10080. 10080 : $(elem).parent().find('.mailtoprotohandler-status').html(status);
  10081. 10081 : }
  10082. 10082 : else {
  10083. 10083 : $(elem).click(function() { ref.register_protocol_handler(name); return false; });
  10084. 10084 : }
  10085. 10085 : };
  10086. 10086 :
  10087. 10087 : // Checks browser capabilities e.g. PDF support, TIF support
  10088. 10088 : this.browser_capabilities_check = function()
  10089. 10089 : {
  10090. 10090 : if (!this.env.browser_capabilities)
  10091. 10091 : this.env.browser_capabilities = {};
  10092. 10092 :
  10093. 10093 : $.each(['pdf', 'flash', 'tiff', 'webp', 'pgpmime'], function() {
  10094. 10094 : if (ref.env.browser_capabilities[this] === undefined)
  10095. 10095 : ref.env.browser_capabilities[this] = ref[this + '_support_check']();
  10096. 10096 : });
  10097. 10097 : };
  10098. 10098 :
  10099. 10099 : // Returns browser capabilities string
  10100. 10100 : this.browser_capabilities = function()
  10101. 10101 : {
  10102. 10102 : if (!this.env.browser_capabilities)
  10103. 10103 : return '';
  10104. 10104 :
  10105. 10105 : var n, ret = [];
  10106. 10106 :
  10107. 10107 : for (n in this.env.browser_capabilities)
  10108. 10108 : ret.push(n + '=' + this.env.browser_capabilities[n]);
  10109. 10109 :
  10110. 10110 : return ret.join();
  10111. 10111 : };
  10112. 10112 :
  10113. 10113 : this.tiff_support_check = function()
  10114. 10114 : {
  10115. 10115 : this.image_support_check('tiff');
  10116. 10116 : return 0;
  10117. 10117 : };
  10118. 10118 :
  10119. 10119 : this.webp_support_check = function()
  10120. 10120 : {
  10121. 10121 : this.image_support_check('webp');
  10122. 10122 : return 0;
  10123. 10123 : };
  10124. 10124 :
  10125. 10125 : this.image_support_check = function(type)
  10126. 10126 : {
  10127. 10127 : setTimeout(function() {
  10128. 10128 : var img = new Image();
  10129. 10129 : img.onload = function() { ref.env.browser_capabilities[type] = 1; };
  10130. 10130 : img.onerror = function() { ref.env.browser_capabilities[type] = 0; };
  10131. 10131 : img.src = ref.assets_path('program/resources/blank.' + type);
  10132. 10132 : }, 10);
  10133. 10133 : };
  10134. 10134 :
  10135. 10135 : this.pdf_support_check = function()
  10136. 10136 : {
  10137. 10137 : var i, plugin = navigator.mimeTypes ? navigator.mimeTypes["application/pdf"] : {},
  10138. 10138 : plugins = navigator.plugins,
  10139. 10139 : len = plugins.length,
  10140. 10140 : regex = /Adobe Reader|PDF|Acrobat/i;
  10141. 10141 :
  10142. 10142 : if (plugin && plugin.enabledPlugin)
  10143. 10143 : return 1;
  10144. 10144 :
  10145. 10145 : if ('ActiveXObject' in window) {
  10146. 10146 : try {
  10147. 10147 : if (plugin = new ActiveXObject("AcroPDF.PDF"))
  10148. 10148 : return 1;
  10149. 10149 : }
  10150. 10150 : catch (e) {}
  10151. 10151 : try {
  10152. 10152 : if (plugin = new ActiveXObject("PDF.PdfCtrl"))
  10153. 10153 : return 1;
  10154. 10154 : }
  10155. 10155 : catch (e) {}
  10156. 10156 : }
  10157. 10157 :
  10158. 10158 : for (i=0; i<len; i++) {
  10159. 10159 : plugin = plugins[i];
  10160. 10160 : if (typeof plugin === 'string') {
  10161. 10161 : if (regex.test(plugin))
  10162. 10162 : return 1;
  10163. 10163 : }
  10164. 10164 : else if (plugin.name && regex.test(plugin.name))
  10165. 10165 : return 1;
  10166. 10166 : }
  10167. 10167 :
  10168. 10168 : setTimeout(function() {
  10169. 10169 : $('<object>').attr({
  10170. 10170 : data: ref.assets_path('program/resources/dummy.pdf'),
  10171. 10171 : type: 'application/pdf',
  10172. 10172 : style: 'position: "absolute"; top: -1000px; height: 1px; width: 1px'
  10173. 10173 : })
  10174. 10174 : .on('load error', function(e) {
  10175. 10175 : ref.env.browser_capabilities.pdf = e.type == 'load' ? 1 : 0;
  10176. 10176 :
  10177. 10177 : // add a short delay before attempting to remove element (#8128)
  10178. 10178 : var obj = this;
  10179. 10179 : setTimeout(function() { $(obj).remove(); }, 10);
  10180. 10180 : })
  10181. 10181 : .appendTo(document.body);
  10182. 10182 : }, 10);
  10183. 10183 :
  10184. 10184 : return 0;
  10185. 10185 : };
  10186. 10186 :
  10187. 10187 : this.flash_support_check = function()
  10188. 10188 : {
  10189. 10189 : var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/x-shockwave-flash"] : {};
  10190. 10190 :
  10191. 10191 : if (plugin && plugin.enabledPlugin)
  10192. 10192 : return 1;
  10193. 10193 :
  10194. 10194 : if ('ActiveXObject' in window) {
  10195. 10195 : try {
  10196. 10196 : if (plugin = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"))
  10197. 10197 : return 1;
  10198. 10198 : }
  10199. 10199 : catch (e) {}
  10200. 10200 : }
  10201. 10201 :
  10202. 10202 : return 0;
  10203. 10203 : };
  10204. 10204 :
  10205. 10205 : // check for mailvelope API
  10206. 10206 : this.pgpmime_support_check = function(action)
  10207. 10207 : {
  10208. 10208 : if (window.mailvelope)
  10209. 10209 : return 1;
  10210. 10210 :
  10211. 10211 : $(window).on('mailvelope', function() {
  10212. 10212 : ref.env.browser_capabilities['pgpmime'] = 1;
  10213. 10213 : });
  10214. 10214 :
  10215. 10215 : return 0;
  10216. 10216 : };
  10217. 10217 :
  10218. 10218 : this.assets_path = function(path)
  10219. 10219 : {
  10220. 10220 : if (this.env.assets_path && !path.startsWith(this.env.assets_path)) {
  10221. 10221 : path = this.env.assets_path + path;
  10222. 10222 : }
  10223. 10223 :
  10224. 10224 : return path;
  10225. 10225 : };
  10226. 10226 :
  10227. 10227 : // Cookie setter
  10228. 10228 : this.set_cookie = function(name, value, expires)
  10229. 10229 : {
  10230. 10230 : if (expires === false) {
  10231. 10231 : var expires = new Date();
  10232. 10232 : expires.setYear(expires.getFullYear() + 1);
  10233. 10233 : }
  10234. 10234 :
  10235. 10235 : setCookie(name, value, expires, this.env.cookie_path, this.env.cookie_domain, this.env.cookie_secure);
  10236. 10236 : };
  10237. 10237 :
  10238. 10238 : this.get_local_storage_prefix = function()
  10239. 10239 : {
  10240. 10240 : if (!this.local_storage_prefix)
  10241. 10241 : this.local_storage_prefix = 'roundcube.' + (this.env.user_id || 'anonymous') + '.';
  10242. 10242 :
  10243. 10243 : return this.local_storage_prefix;
  10244. 10244 : };
  10245. 10245 :
  10246. 10246 : // wrapper for localStorage.getItem(key)
  10247. 10247 : this.local_storage_get_item = function(key, deflt, encrypted)
  10248. 10248 : {
  10249. 10249 : var item, result;
  10250. 10250 :
  10251. 10251 : // TODO: add encryption
  10252. 10252 : try {
  10253. 10253 : item = localStorage.getItem(this.get_local_storage_prefix() + key);
  10254. 10254 : result = JSON.parse(item);
  10255. 10255 : }
  10256. 10256 : catch (e) { }
  10257. 10257 :
  10258. 10258 : return result || deflt || null;
  10259. 10259 : };
  10260. 10260 :
  10261. 10261 : // wrapper for localStorage.setItem(key, data)
  10262. 10262 : this.local_storage_set_item = function(key, data, encrypted)
  10263. 10263 : {
  10264. 10264 : // try/catch to handle no localStorage support, but also error
  10265. 10265 : // in Safari-in-private-browsing-mode where localStorage exists
  10266. 10266 : // but can't be used (#1489996)
  10267. 10267 : try {
  10268. 10268 : // TODO: add encryption
  10269. 10269 : localStorage.setItem(this.get_local_storage_prefix() + key, JSON.stringify(data));
  10270. 10270 : return true;
  10271. 10271 : }
  10272. 10272 : catch (e) {
  10273. 10273 : return false;
  10274. 10274 : }
  10275. 10275 : };
  10276. 10276 :
  10277. 10277 : // wrapper for localStorage.removeItem(key)
  10278. 10278 : this.local_storage_remove_item = function(key)
  10279. 10279 : {
  10280. 10280 : try {
  10281. 10281 : localStorage.removeItem(this.get_local_storage_prefix() + key);
  10282. 10282 : return true;
  10283. 10283 : }
  10284. 10284 : catch (e) {
  10285. 10285 : return false;
  10286. 10286 : }
  10287. 10287 : };
  10288. 10288 :
  10289. 10289 : this.print_dialog = function()
  10290. 10290 : {
  10291. 10291 : // setTimeout for Safari
  10292. 10292 : setTimeout('window.print()', 10);
  10293. 10293 : };
  10294. 10294 : } // end object rcube_webmail
  10295. 10295 :
  10296. 10296 :
  10297. 10297 : // some static methods
  10298. 10298 : rcube_webmail.long_subject_title = function(elem, indent, text_elem)
  10299. 10299 : {
  10300. 10300 : if (!elem.title) {
  10301. 10301 : var $elem = $(text_elem || elem);
  10302. 10302 : if ($elem.width() + (indent || 0) * 15 > $elem.parent().width())
  10303. 10303 : elem.title = rcube_webmail.subject_text($elem[0]);
  10304. 10304 : }
  10305. 10305 : };
  10306. 10306 :
  10307. 10307 : rcube_webmail.long_subject_title_ex = function(elem)
  10308. 10308 : {
  10309. 10309 : if (!elem.title) {
  10310. 10310 : var $elem = $(elem),
  10311. 10311 : txt = $elem.text().trim(),
  10312. 10312 : indent = $('span.branch', $elem).width() || 0,
  10313. 10313 : tmp = $('<span>').text(txt)
  10314. 10314 : .css({position: 'absolute', 'float': 'left', visibility: 'hidden',
  10315. 10315 : 'font-size': $elem.css('font-size'), 'font-weight': $elem.css('font-weight')})
  10316. 10316 : .appendTo(document.body),
  10317. 10317 : w = tmp.width();
  10318. 10318 :
  10319. 10319 : tmp.remove();
  10320. 10320 : if (w + indent * 15 > $elem.width())
  10321. 10321 : elem.title = rcube_webmail.subject_text(elem);
  10322. 10322 : }
  10323. 10323 : };
  10324. 10324 :
  10325. 10325 : rcube_webmail.subject_text = function(elem)
  10326. 10326 : {
  10327. 10327 : var t = $(elem).clone();
  10328. 10328 : t.find('.skip-on-drag,.skip-content,.voice').remove();
  10329. 10329 : return t.text().trim();
  10330. 10330 : };
  10331. 10331 :
  10332. 10332 : // set event handlers on all iframe elements (and their contents)
  10333. 10333 : rcube_webmail.set_iframe_events = function(events)
  10334. 10334 : {
  10335. 10335 : $('iframe').each(function() {
  10336. 10336 : var frame = $(this);
  10337. 10337 : $.each(events, function(event_name, event_handler) {
  10338. 10338 : frame.on('load', function(e) {
  10339. 10339 : try { $(this).contents().on(event_name, event_handler); }
  10340. 10340 : catch (e) {/* catch possible permission error in IE */ }
  10341. 10341 : });
  10342. 10342 :
  10343. 10343 : try { frame.contents().on(event_name, event_handler); }
  10344. 10344 : catch (e) {/* catch possible permission error in IE */ }
  10345. 10345 : });
  10346. 10346 : });
  10347. 10347 : };
  10348. 10348 :
  10349. 10349 : rcube_webmail.prototype.get_cookie = getCookie;
  10350. 10350 :
  10351. 10351 : // copy event engine prototype
  10352. 10352 : rcube_webmail.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
  10353. 10353 : rcube_webmail.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
  10354. 10354 : rcube_webmail.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;