1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.0 (the "License"); 7 * you may not use this file except in compliance with the License. 8 * You may obtain a copy of the License at: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * Creates an empty list of mail items. 26 * @constructor 27 * @class 28 * This class represents a list of mail items (conversations, messages, or 29 * attachments). We retain a handle to the search that generated the list for 30 * two reasons: so that we can redo the search if necessary, and so that we 31 * can get the folder ID if this list represents folder contents. 32 * 33 * @author Conrad Damon 34 * 35 * @param type type of mail item (see ZmItem for constants) 36 * @param search the search that generated this list 37 */ 38 ZmMailList = function(type, search) { 39 40 ZmList.call(this, type, search); 41 42 this.convId = null; // for msg list within a conv 43 44 // mail list can be changed via folder or tag action (eg "Mark All Read") 45 var folderTree = appCtxt.getFolderTree(); 46 if (folderTree) { 47 this._folderChangeListener = new AjxListener(this, this._folderTreeChangeListener); 48 folderTree.addChangeListener(this._folderChangeListener); 49 } 50 }; 51 52 ZmMailList.prototype = new ZmList; 53 ZmMailList.prototype.constructor = ZmMailList; 54 55 ZmMailList.prototype.isZmMailList = true; 56 ZmMailList.prototype.toString = function() { return "ZmMailList"; }; 57 58 ZmMailList._SPECIAL_FOLDERS = [ZmFolder.ID_DRAFTS, ZmFolder.ID_TRASH, ZmFolder.ID_SPAM, ZmFolder.ID_SENT]; 59 ZmMailList._SPECIAL_FOLDERS_HASH = AjxUtil.arrayAsHash(ZmMailList._SPECIAL_FOLDERS); 60 61 62 /** 63 * Override so that we can specify "tcon" attribute for conv move - we don't want 64 * to move messages in certain system folders as a side effect. Also, we need to 65 * update the UI based on the response if we're moving convs, since the 66 * notifications only tell us about moved messages. This method should be called 67 * only in response to explicit action by the user, in which case we want to 68 * remove the conv row(s) from the list view (even if the conv still matches the 69 * search). 70 * 71 * @param {Hash} params a hash of parameters 72 * items [array] a list of items to move 73 * folder [ZmFolder] destination folder 74 * attrs [hash] additional attrs for SOAP command 75 * callback [AjxCallback]* callback to run after each sub-request 76 * finalCallback [closure]* callback to run after all items have been processed 77 * count [int]* starting count for number of items processed 78 * fromFolderId [String]* optional folder to represent when calculating tcon. If unspecified, use current search folder nId 79 * 80 * @private 81 */ 82 ZmMailList.prototype.moveItems = 83 function(params) { 84 85 if (this.type != ZmItem.CONV) { 86 return ZmList.prototype.moveItems.apply(this, arguments); 87 } 88 89 params = Dwt.getParams(arguments, ["items", "folder", "attrs", "callback", "finalCallback", "noUndo", "actionTextKey", "fromFolderId"]); 90 params.items = AjxUtil.toArray(params.items); 91 92 var params1 = AjxUtil.hashCopy(params); 93 delete params1.fromFolderId; 94 95 params1.attrs = {}; 96 var tcon = this._getTcon(params.items, params.fromFolderId); 97 if (tcon) { 98 params1.attrs.tcon = tcon; 99 } 100 params1.attrs.l = params.folder.id; 101 params1.action = (params.folder.id == ZmFolder.ID_TRASH) ? "trash" : "move"; 102 if (params1.folder.id == ZmFolder.ID_TRASH) { 103 params1.actionTextKey = params.actionTextKey || "actionTrash"; 104 } else { 105 params1.actionTextKey = params.actionTextKey || "actionMove"; 106 params1.actionArg = params1.folder.getName(false, false, true); 107 } 108 params1.callback = new AjxCallback(this, this._handleResponseMoveItems, [params]); 109 110 if (appCtxt.multiAccounts) { 111 // Reset accountName for multi-account to be the respective account if we're 112 // moving a draft out of Trash. 113 // OR, 114 // check if we're moving to or from a shared folder, in which case, always send 115 // request on-behalf-of the account the item originally belongs to. 116 var folderId = params.items[0].getFolderId && params.items[0].getFolderId(); 117 118 // on bulk delete, when the second chunk loads try to get folderId from the item id. 119 if (!folderId) { 120 var itemId = params.items[0] && params.items[0].id; 121 folderId = itemId && appCtxt.getById(itemId) && appCtxt.getById(itemId).folderId; 122 } 123 var fromFolder = folderId && appCtxt.getById(folderId); 124 if ((params.items[0].isDraft && params.folder.id == ZmFolder.ID_DRAFTS) || 125 (params.folder.isRemote()) || (fromFolder && fromFolder.isRemote())) 126 { 127 params1.accountName = params.items[0].getAccount().name; 128 } 129 } 130 131 if (this._handleDeleteFromSharedFolder(params, params1)) { 132 return; 133 } 134 135 params1.safeMove = true; //Move only items currently seen by the client 136 this._itemAction(params1); 137 }; 138 139 /** 140 * Marks items as "spam" or "not spam". If they're marked as "not spam", a target folder 141 * may be provided. 142 * @param {Hash} params a hash of parameters 143 * items [array] a list of items 144 * markAsSpam [boolean] if true, mark as "spam" 145 * folder [ZmFolder] destination folder 146 * childWin [window]* the child window this action is happening in 147 * closeChildWin [boolean]* is the child window closed at the end of the action? 148 * callback [AjxCallback]* callback to run after each sub-request 149 * finalCallback [closure]* callback to run after all items have been processed 150 * count [int]* starting count for number of items processed 151 * @private 152 */ 153 ZmMailList.prototype.spamItems = 154 function(params) { 155 156 var items = params.items = AjxUtil.toArray(params.items); 157 158 if (appCtxt.multiAccounts) { 159 var accounts = this._filterItemsByAccount(items); 160 this._spamAccountItems(accounts, params); 161 } else { 162 this._spamItems(params); 163 } 164 }; 165 166 ZmMailList.prototype._spamAccountItems = 167 function(accounts, params) { 168 var items; 169 for (var i in accounts) { 170 items = accounts[i]; 171 break; 172 } 173 174 if (items) { 175 delete accounts[i]; 176 177 params.accountName = appCtxt.accountList.getAccount(i).name; 178 params.items = items; 179 params.callback = new AjxCallback(this, this._spamAccountItems, [accounts, params]); 180 181 this._spamItems(params); 182 } 183 }; 184 185 ZmMailList.prototype._spamItems = 186 function(params) { 187 params = Dwt.getParams(arguments, ["items", "markAsSpam", "folder", "childWin"]); 188 189 var params1 = AjxUtil.hashCopy(params); 190 191 params1.action = params.markAsSpam ? "spam" : "!spam"; 192 params1.attrs = {}; 193 if (this.type === ZmItem.CONV) { 194 var tcon = this._getTcon(params.items); 195 //the reason not to set "" as tcon is from bug 58727. (though I think it should have been a server fix). 196 if (tcon) { 197 params1.attrs.tcon = tcon; 198 } 199 } 200 if (params.folder) { 201 params1.attrs.l = params.folder.id; 202 } 203 params1.actionTextKey = params.markAsSpam ? 'actionMarkAsJunk' : 'actionMarkAsNotJunk'; 204 205 params1.callback = new AjxCallback(this, this._handleResponseSpamItems, params); 206 this._itemAction(params1); 207 }; 208 209 ZmMailList.prototype._handleResponseSpamItems = 210 function(params, result) { 211 212 var movedItems = result.getResponse(); 213 var summary; 214 if (movedItems && movedItems.length) { 215 var folderId = params.markAsSpam ? ZmFolder.ID_SPAM : (params.folder ? params.folder.id : ZmFolder.ID_INBOX); 216 this.moveLocal(movedItems, folderId); 217 var convs = {}; 218 for (var i = 0; i < movedItems.length; i++) { 219 var item = movedItems[i]; 220 if (item.cid) { 221 var conv = appCtxt.getById(item.cid); 222 if (conv) { 223 if (!convs[conv.id]) 224 convs[conv.id] = {conv:conv,msgs:[]}; 225 convs[conv.id].msgs.push(item); 226 } 227 } 228 var details = {oldFolderId:item.folderId, fields:{}}; 229 details.fields[ZmItem.F_FRAGMENT] = true; 230 item.moveLocal(folderId); 231 } 232 233 for (var id in convs) { 234 if (convs.hasOwnProperty(id)) { 235 var conv = convs[id].conv; 236 var msgs = convs[id].msgs; 237 conv.updateFragment(msgs); 238 } 239 } 240 //ZmModel.notifyEach(movedItems, ZmEvent.E_MOVE); 241 242 var item = movedItems[0]; 243 var list = item.list; 244 if (list) { 245 list._evt.batchMode = true; 246 list._evt.item = item; // placeholder 247 list._evt.items = movedItems; 248 list._notify(ZmEvent.E_MOVE, details); 249 } 250 if (params.actionText) { 251 summary = ZmList.getActionSummary(params); 252 } 253 254 if (params.childWin) { 255 params.childWin.close(); 256 } 257 } 258 params.actionSummary = summary; 259 if (params.callback) { 260 params.callback.run(result); 261 } 262 }; 263 264 /** 265 * Override so that delete of a conv in Trash doesn't hard-delete its msgs in 266 * other folders. If we're in conv mode in Trash, we add a constraint of "t", 267 * meaning that the action is only applied to items (msgs) in the Trash. 268 * 269 * @param {Hash} params a hash of parameters 270 * @param {Array} params.items list of items to delete 271 * @param {Boolean} params.hardDelete whether to force physical removal of items 272 * @param {Object} params.attrs additional attrs for SOAP command 273 * @param {window} params.childWin the child window this action is happening in 274 * @param {Boolean} params.confirmDelete the user confirmed hard delete 275 * 276 * @private 277 */ 278 ZmMailList.prototype.deleteItems = 279 function(params) { 280 281 params = Dwt.getParams(arguments, ["items", "hardDelete", "attrs", "childWin"]); 282 283 if (this.type == ZmItem.CONV) { 284 var searchFolder = this.search ? appCtxt.getById(this.search.folderId) : null; 285 if (searchFolder && searchFolder.isHardDelete()) { 286 287 if (!params.confirmDelete) { 288 params.confirmDelete = true; 289 var callback = ZmMailList.prototype.deleteItems.bind(this, params); 290 this._popupDeleteWarningDialog(callback, false, params.items.length); 291 return; 292 } 293 294 var instantOn = appCtxt.getAppController().getInstantNotify(); 295 if (instantOn) { 296 // bug fix #32005 - disable instant notify for ops that might take awhile 297 appCtxt.getAppController().setInstantNotify(false); 298 params.errorCallback = new AjxCallback(this, this._handleErrorDeleteItems); 299 } 300 301 params.attrs = params.attrs || {}; 302 params.attrs.tcon = ZmFolder.TCON_CODE[searchFolder.nId]; 303 params.action = "delete"; 304 params.actionTextKey = 'actionDelete'; 305 params.callback = new AjxCallback(this, this._handleResponseDeleteItems, instantOn); 306 return this._itemAction(params); 307 } 308 } 309 ZmList.prototype.deleteItems.call(this, params); 310 }; 311 312 ZmMailList.prototype._handleResponseDeleteItems = 313 function(instantOn, result) { 314 var deletedItems = result.getResponse(); 315 if (deletedItems && deletedItems.length) { 316 this.deleteLocal(deletedItems); 317 for (var i = 0; i < deletedItems.length; i++) { 318 deletedItems[i].deleteLocal(); 319 } 320 // note: this happens before we process real notifications 321 ZmModel.notifyEach(deletedItems, ZmEvent.E_DELETE); 322 } 323 324 if (instantOn) { 325 appCtxt.getAppController().setInstantNotify(true); 326 } 327 }; 328 329 ZmMailList.prototype._handleErrorDeleteItems = 330 function() { 331 appCtxt.getAppController().setInstantNotify(true); 332 }; 333 334 /** 335 * Only make the request for items whose state will be changed. 336 * 337 * @param {Hash} params a hash of parameters 338 * 339 * items [array] a list of items to mark read/unread 340 * value [boolean] if true, mark items read 341 * callback [AjxCallback]* callback to run after each sub-request 342 * finalCallback [closure]* callback to run after all items have been processed 343 * count [int]* starting count for number of items processed 344 * 345 * @private 346 */ 347 ZmMailList.prototype.markRead = 348 function(params) { 349 350 var items = AjxUtil.toArray(params.items); 351 352 var items1; 353 if (items[0] && items[0] instanceof ZmItem) { 354 items1 = []; 355 for (var i = 0; i < items.length; i++) { 356 var item = items[i]; 357 if ((item.type == ZmItem.CONV && item.hasFlag(ZmItem.FLAG_UNREAD, params.value)) || (item.isUnread == params.value)) { 358 items1.push(item); 359 } 360 } 361 } else { 362 items1 = items; 363 } 364 365 if (items1.length) { 366 params.items = items1; 367 params.op = "read"; 368 if (items1.length > 1) { 369 params.actionTextKey = params.value ? 'actionMarkRead' : 'actionMarkUnread'; 370 } 371 this.flagItems(params); 372 } 373 else if(params.forceCallback) { 374 if (params.callback) { 375 params.callback.run(new ZmCsfeResult([])); 376 } 377 if (params.finalCallback) { 378 params.finalCallback(params); 379 } 380 return; 381 } 382 }; 383 384 /** 385 * Only make the request for items whose state will be changed. 386 * 387 * @param {Hash} params a hash of parameters 388 * 389 * items [array] a list of items to mark read/unread 390 * value [boolean] if true, mark items read 391 * callback [AjxCallback]* callback to run after each sub-request 392 * finalCallback [closure]* callback to run after all items have been processed 393 * count [int]* starting count for number of items processed 394 * 395 * @private 396 */ 397 ZmMailList.prototype.markMute = 398 function(params) { 399 400 var items = AjxUtil.toArray(params.items); 401 402 var items1; 403 if (items[0] && items[0] instanceof ZmItem) { 404 items1 = []; 405 for (var i = 0; i < items.length; i++) { 406 var item = items[i]; 407 if (params.value != item.isMute) { 408 items1.push(item); 409 } 410 } 411 } else { 412 items1 = items; 413 } 414 415 if (items1.length) { 416 params.items = items1; 417 params.op = "mute"; 418 params.actionTextKey = params.value ? 'actionMarkMute' : 'actionMarkUnmute'; 419 this.flagItems(params); 420 } 421 else if(params.forceCallback) { 422 if (params.callback) { 423 params.callback.run(new ZmCsfeResult([])); 424 } 425 if (params.finalCallback) { 426 params.finalCallback(params); 427 } 428 return; 429 } 430 }; 431 432 // set "force" flag to true on actual hard deletes, so that msgs 433 // in a conv list are removed 434 ZmMailList.prototype.deleteLocal = 435 function(items) { 436 for (var i = 0; i < items.length; i++) { 437 this.remove(items[i], true); 438 } 439 }; 440 441 // When a conv or msg is moved to Trash, it is marked read by the server. 442 ZmMailList.prototype.moveLocal = 443 function(items, folderId) { 444 ZmList.prototype.moveLocal.call(this, items, folderId); 445 if (folderId != ZmFolder.ID_TRASH) { return; } 446 447 var flaggedItems = []; 448 for (var i = 0; i < items.length; i++) { 449 if (items[i].isUnread) { 450 items[i].flagLocal(ZmItem.FLAG_UNREAD, false); 451 flaggedItems.push(items[i]); 452 } 453 } 454 ZmModel.notifyEach(flaggedItems, ZmEvent.E_FLAGS, {flags:[ZmItem.FLAG_UNREAD]}); 455 }; 456 457 ZmMailList.prototype.notifyCreate = 458 function(convs, msgs) { 459 460 var createdItems = []; 461 var newConvs = []; 462 var newMsgs = []; 463 var flaggedItems = []; 464 var modifiedItems = []; 465 var newConvId = {}; 466 var fields = {}; 467 var sortBy = this.search ? this.search.sortBy : null; 468 var sortIndex = {}; 469 if (this.type == ZmItem.CONV) { 470 // handle new convs first so we can set their fragments later from new msgs 471 for (var id in convs) { 472 AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: handling conv create " + id); 473 if (this.getById(id)) { 474 AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: conv already exists " + id); 475 continue; 476 } 477 newConvId[id] = convs[id]; 478 var conv = convs[id]; 479 var convMatches = this.search && this.search.matches(conv) && !conv.ignoreJunkTrash(); 480 if (convMatches) { 481 if (!appCtxt.multiAccounts || 482 (appCtxt.multiAccounts && (this.search.isMultiAccount() || conv.getAccount() == appCtxt.getActiveAccount()))) 483 { 484 // a new msg for this conv matches current search 485 conv.list = this; 486 newConvs.push(conv); 487 AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: conv added " + id); 488 } 489 else { 490 AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: conv failed account checks " + id); 491 } 492 } 493 else { 494 // debug info for bug 47589 495 var query = this.search ? this.search.query : ""; 496 var ignore = conv.ignoreJunkTrash(); 497 AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: conv does not match search '" + query + "' or was ignored (" + ignore + "); match function:"); 498 if (!conv) { 499 AjxDebug.println(AjxDebug.NOTIFY, "conv is null!"); 500 } 501 else { 502 var folders = AjxUtil.keys(conv.folders) || ""; 503 AjxDebug.println(AjxDebug.NOTIFY, "conv folders: " + folders.join(" ")); 504 } 505 } 506 } 507 508 // a new msg can hand us a new conv, and update a conv's info 509 for (var id in msgs) { 510 var msg = msgs[id]; 511 AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: CLV handling msg create " + id); 512 var cid = msg.cid; 513 var msgMatches = this.search && this.search.matches(msg) && !msg.ignoreJunkTrash(); 514 AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: CLV msg matches: " + msgMatches); 515 var isActiveAccount = (!appCtxt.multiAccounts || (appCtxt.multiAccounts && msg.getAccount() == appCtxt.getActiveAccount())); 516 var conv = newConvId[cid] || this.getById(cid); 517 var updateConv = false; 518 if (msgMatches && isActiveAccount) { 519 if (!conv) { 520 // msg will have _convCreateNode if it is 2nd msg and caused promotion of virtual conv; 521 // the conv node will have proper count and subject 522 var args = {list:this}; 523 if (msg._convCreateNode) { 524 if (msg._convCreateNode._newId) { 525 msg._convCreateNode.id = msg._convCreateNode._newId; 526 } 527 //sometimes the conv is already in the app cache. Make sure not to re-create it and with the wrong msgs. This is slight improvement of bug 87861. 528 conv = appCtxt.getById(cid); 529 if (!conv) { 530 conv = ZmConv.createFromDom(msg._convCreateNode, args); 531 } 532 } 533 else { 534 conv = appCtxt.getById(cid) || ZmConv.createFromMsg(msg, args); 535 } 536 newConvId[cid] = conv; 537 conv.folders[msg.folderId] = true; 538 newConvs.push(conv); 539 } 540 conv.list = this; 541 } 542 // make sure conv's msg list is up to date 543 if (conv && !(conv.msgs && conv.msgs.getById(id))) { 544 if (!conv.msgs) { 545 conv.msgs = new ZmMailList(ZmItem.MSG); 546 conv.msgs.addChangeListener(conv._listChangeListener); 547 } 548 msg.list = conv.msgs; 549 if (!msg.isSent && msg.isUnread) { 550 conv.isUnread = true; 551 flaggedItems.push(conv); 552 } 553 // if the new msg matches current search, update conv date, fragment, and sort order 554 if (msgMatches) { 555 msg.inHitList = true; 556 } 557 if (msgMatches || ((msgMatches === null) && !msg.isSent)) { 558 if (conv.fragment != msg.fragment) { 559 conv.fragment = msg.fragment; 560 fields[ZmItem.F_FRAGMENT] = true; 561 } 562 if (conv.date != msg.date) { 563 conv.date = msg.date; 564 // recalculate conv's sort position since we changed its date 565 fields[ZmItem.F_DATE] = true; 566 } 567 if (conv.numMsgs === 1) { 568 //there is only one message in this conv so set the size of conv to msg size 569 conv.size = msg.size; 570 } 571 else { 572 //So it shows the message count, and not the size (see ZmConvListView.prototype._getCellContents) 573 //this size is no longer relevant (was set in the above if previously, see bug 87416) 574 conv.size = null; 575 } 576 if (msg._convCreateNode) { 577 //in case of single msg virtual conv promoted to a real conv - update the size 578 // (in other cases of size it's updated elsewhere - see ZmConv.prototype.notifyModify, the server sends the update notification for the conv size) 579 fields[ZmItem.F_SIZE] = true; 580 } 581 // conv gained a msg, may need to be moved to top/bottom 582 if (!newConvId[conv.id] && this._vector.contains(conv)) { 583 fields[ZmItem.F_INDEX] = true; 584 } 585 modifiedItems.push(conv); 586 } 587 AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: conv list accepted msg " + id); 588 newMsgs.push(msg); 589 } 590 } 591 } else if (this.type == ZmItem.MSG) { 592 // add new msg to list 593 for (var id in msgs) { 594 var msg = msgs[id]; 595 var msgMatches = this.search && this.search.matches(msg) && !msg.ignoreJunkTrash(); 596 AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: handling msg create " + id); 597 if (this.getById(id)) { 598 if (msgMatches) { 599 var query = this.search ? this.search.query : ""; 600 var ignore = msg.ignoreJunkTrash(); 601 AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: msg does not match search '" + query + "' or was ignored (" + ignore + ")"); 602 msg.list = this; // Even though we have the msg in the list, it sometimes has its list wrong. 603 } 604 continue; 605 } 606 if (this.convId) { // MLV within CV 607 if (msg.cid == this.convId && !this.getById(msg.id)) { 608 msg.list = this; 609 AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: msg list (CV) accepted msg " + id); 610 newMsgs.push(msg); 611 } 612 } else { // MLV (traditional) 613 if (msgMatches) { 614 msg.list = this; 615 AjxDebug.println(AjxDebug.NOTIFY, "ZmMailList: msg list (TV) accepted msg " + id); 616 newMsgs.push(msg); 617 } 618 } 619 } 620 } 621 622 // sort item list in reverse so they show up in correct order when processed (oldest appears first) 623 if (newConvs.length > 1) { 624 ZmMailItem.sortBy = sortBy; 625 newConvs.sort(ZmMailItem.sortCompare); 626 newConvs.reverse(); 627 } 628 629 this._sortAndNotify(newConvs, sortBy, ZmEvent.E_CREATE); 630 this._sortAndNotify(newMsgs, sortBy, ZmEvent.E_CREATE); 631 ZmModel.notifyEach(flaggedItems, ZmEvent.E_FLAGS, {flags:[ZmItem.FLAG_UNREAD]}); 632 this._sortAndNotify(modifiedItems, sortBy, ZmEvent.E_MODIFY, {fields:fields}); 633 this._sortAndNotify(newMsgs, sortBy, ZmEvent.E_MODIFY, {fields:fields}); 634 }; 635 636 /** 637 * Convenience method for adding messages to a conv on the fly. The specific use case for 638 * this is when a virtual conv becomes real. We basically add the new message(s) to the 639 * old (virtual) conv's message list. 640 * 641 * @param msgs hash of messages to add 642 */ 643 ZmMailList.prototype.addMsgs = 644 function(msgs) { 645 var addedMsgs = []; 646 for (var id in msgs) { 647 var msg = msgs[id]; 648 if (msg.cid == this.convId) { 649 this.add(msg, 0); 650 msg.list = this; 651 addedMsgs.push(msg); 652 } 653 } 654 ZmModel.notifyEach(addedMsgs, ZmEvent.E_CREATE); 655 }; 656 657 658 ZmMailList.prototype.removeAllItems = 659 function() { 660 this._vector = new AjxVector(); 661 this._idHash = {}; 662 }; 663 664 665 ZmMailList.prototype.remove = 666 function(item, force) { 667 // Don't really remove an item if this is a list of msgs of a conv b/c a 668 // msg is always going to be part of a conv unless it's a hard delete! 669 if (!this.convId || force) { 670 ZmList.prototype.remove.call(this, item); 671 } 672 }; 673 674 ZmMailList.prototype.clear = 675 function() { 676 // remove listeners for this list from folder tree and tag list 677 if (this._folderChangeListener) { 678 var folderTree = appCtxt.getFolderTree(); 679 if (folderTree) { 680 folderTree.removeChangeListener(this._folderChangeListener); 681 } 682 } 683 if (this._tagChangeListener) { 684 var tagTree = appCtxt.getTagTree(); 685 if (tagTree) { 686 tagTree.removeChangeListener(this._tagChangeListener); 687 } 688 } 689 690 ZmList.prototype.clear.call(this); 691 }; 692 693 /** 694 * Gets the first msg in the list that's not in one of the given folders (if any). 695 * 696 * @param {int} offset the starting point within list 697 * @param {int} limit the ending point within list 698 * @param {foldersToOmit} A hash of folders to omit 699 * @return {ZmMailMsg} the message 700 */ 701 ZmMailList.prototype.getFirstHit = 702 function(offset, limit, foldersToOmit) { 703 704 if (this.type !== ZmItem.MSG) { 705 return null; 706 } 707 708 var msg = null; 709 offset = offset || 0; 710 limit = limit || appCtxt.get(ZmSetting.CONVERSATION_PAGE_SIZE); 711 var numMsgs = this.size(); 712 713 if (numMsgs > 0 && offset >= 0 && offset < numMsgs) { 714 var end = (offset + limit > numMsgs) ? numMsgs : offset + limit; 715 var list = this.getArray(); 716 for (var i = offset; i < end; i++) { 717 if (!(foldersToOmit && list[i].folderId && foldersToOmit[list[i].folderId])) { 718 msg = list[i]; 719 break; 720 } 721 } 722 if (!msg) { 723 msg = list[0]; // no qualifying messages, use first msg 724 } 725 } 726 727 return msg; 728 }; 729 730 /** 731 * Returns the insertion point for the given item into this list. If we're not sorting by 732 * date, returns 0 (the item will be inserted at the top of the list). 733 * 734 * @param item [ZmMailItem] a mail item 735 * @param sortBy [constant] sort order 736 */ 737 ZmMailList.prototype._getSortIndex = 738 function(item, sortBy) { 739 if (!sortBy || (sortBy != ZmSearch.DATE_DESC && sortBy != ZmSearch.DATE_ASC)) { 740 return 0; 741 } 742 743 var itemDate = parseInt(item.date); 744 var a = this.getArray(); 745 // server always orders conv's msg list as DATE_DESC 746 if (this.convId && sortBy == ZmSearch.DATE_ASC) { 747 //create a temp array with reverse index and date 748 var temp = []; 749 for(var j = a.length - 1;j >=0;j--) { 750 temp.push({date:a[j].date}); 751 } 752 a = temp; 753 } 754 for (var i = 0; i < a.length; i++) { 755 var date = parseInt(a[i].date); 756 if ((sortBy == ZmSearch.DATE_DESC && (itemDate >= date)) || 757 (sortBy == ZmSearch.DATE_ASC && (itemDate <= date))) { 758 return i; 759 } 760 } 761 return i; 762 }; 763 764 ZmMailList.prototype._sortAndNotify = 765 function(items, sortBy, event, details) { 766 767 if (!(items && items.length)) { return; } 768 769 var itemType = items[0] && items[0].type; 770 if ((this.type == ZmItem.MSG) && (itemType == ZmItem.CONV)) { return; } 771 772 details = details || {}; 773 var doSort = ((event == ZmEvent.E_CREATE) || (details.fields && details.fields[ZmItem.F_DATE])); 774 for (var i = 0; i < items.length; i++) { 775 var item = items[i]; 776 if (doSort) { 777 var doAdd = (itemType == this.type); 778 var listSortIndex = 0, viewSortIndex = 0; 779 if (this.type == ZmItem.CONV && itemType == ZmItem.MSG) { 780 //Bug 87861 - we still want to add the message to the conv even if the conv is not in this view. So look for it in appCtxt cache too. (case in point - it's in "sent" folder) 781 var conv = this.getById(item.cid) || appCtxt.getById(item.cid); 782 if (conv) { 783 // server always orders msgs within a conv by DATE_DESC, so maintain that 784 listSortIndex = conv.msgs._getSortIndex(item, ZmSearch.DATE_DESC); 785 viewSortIndex = conv.msgs._getSortIndex(item, appCtxt.get(ZmSetting.CONVERSATION_ORDER)); 786 if (event == ZmEvent.E_CREATE) { 787 conv.addMsg(item, listSortIndex); 788 } 789 } 790 } else { 791 viewSortIndex = listSortIndex = this._getSortIndex(item, sortBy); 792 } 793 if (event != ZmEvent.E_CREATE) { 794 // if date changed, re-insert item into correct slot 795 if (listSortIndex != this.indexOf(item)) { 796 this.remove(item); 797 } else { 798 doAdd = false; 799 } 800 } 801 if (doAdd) { 802 this.add(item, listSortIndex); 803 } 804 details.sortIndex = viewSortIndex; 805 } 806 item._notify(event, details); 807 } 808 }; 809 810 ZmMailList.prototype._isItemInSpecialFolder = 811 function(item) { 812 // if (item.folderId) { //case of one message in conv, even if not loaded yet, we know the folder. 813 // return ZmMailList._SPECIAL_FOLDERS_HASH[item.folderId]; 814 // } 815 var msgs = item.msgs; 816 if (!msgs) { //might not be loaded yet. In this case, tough luck - the tcon will be set as usual - based on searched folder, if set 817 return false; 818 } 819 for (var i = 0; i < msgs.size(); i++) { 820 var msg = msgs.get(i); 821 var msgFolder = appCtxt.getById(msg.folderId); 822 var msgFolderId = msgFolder && msgFolder.nId; 823 824 if (!ZmMailList._SPECIAL_FOLDERS_HASH[msgFolderId]) { 825 return false; 826 } 827 } 828 return true; 829 }; 830 831 ZmMailList.prototype._getTcon = 832 function(items, nFromFolderId) { 833 834 //if all items are in a special folder (draft/trash/spam/sent) - then just allow the move without any restriction 835 var allItemsSpecial = true; 836 for (var i = 0; i < items.length; i++) { 837 if (!this._isItemInSpecialFolder(items[i])) { 838 allItemsSpecial = false; 839 break; 840 } 841 } 842 843 if (allItemsSpecial) { 844 return ""; 845 } 846 847 var fromFolderId = nFromFolderId || (this.search && this.search.folderId); 848 var fromFolder = fromFolderId && appCtxt.getById(fromFolderId); 849 850 fromFolderId = fromFolder && fromFolder.nId; 851 var tcon = []; 852 for (i = 0; i < ZmMailList._SPECIAL_FOLDERS.length; i++) { 853 var specialFolderId = ZmMailList._SPECIAL_FOLDERS[i]; 854 if (!fromFolder) { 855 tcon.push(ZmFolder.TCON_CODE[specialFolderId]); 856 continue; 857 } 858 // == instead of === since we compare numbers to strings and want conversion. 859 if (fromFolderId == specialFolderId) { 860 continue; //we're moving out of the special folder - allow items under it 861 } 862 var specialFolder; 863 // get folder object from qualified Ids for multi-account 864 if (appCtxt.multiAccounts) { 865 var acct = items && items[0].getAccount && items[0].getAccount(); 866 var acctId = acct ? acct.id : appCtxt.getActiveAccount().id; 867 var fId = [acctId, ":", specialFolderId].join(""); 868 specialFolder = appCtxt.getById(fId); 869 } 870 else { 871 specialFolder = appCtxt.getById(specialFolderId); 872 } 873 874 if (!fromFolder.isChildOf(specialFolder)) { 875 //if origin folder (searched folder) not descendant of the special folder - add the tcon code - don't move items from under the special folder. 876 tcon.push(ZmFolder.TCON_CODE[specialFolderId]); 877 } 878 } 879 return (tcon.length) ? ("-" + tcon.join("")) : ""; 880 }; 881 882 // If this list is the result of a search that is constrained by the read 883 // status, and the user has marked all read in a folder, redo the search. 884 ZmMailList.prototype._folderTreeChangeListener = 885 function(ev) { 886 if (this.size() == 0) { return; } 887 888 var flag = ev.getDetail("flag"); 889 var view = appCtxt.getCurrentViewId(); 890 var ctlr = appCtxt.getCurrentController(); 891 892 if (ev.event == ZmEvent.E_FLAGS && (flag == ZmItem.FLAG_UNREAD)) { 893 if (this.type == ZmItem.CONV) { 894 if ((view == ZmId.VIEW_CONVLIST) && ctlr._currentSearch.hasUnreadTerm()) { 895 this._redoSearch(ctlr); 896 } 897 } else if (this.type == ZmItem.MSG) { 898 if (view == ZmId.VIEW_TRAD && ctlr._currentSearch.hasUnreadTerm()) { 899 this._redoSearch(ctlr); 900 } else { 901 var on = ev.getDetail("state"); 902 var organizer = ev.getDetail("item"); 903 var flaggedItems = []; 904 var list = this.getArray(); 905 for (var i = 0; i < list.length; i++) { 906 var msg = list[i]; 907 if ((organizer.type == ZmOrganizer.FOLDER && msg.folderId == organizer.id) || 908 (organizer.type == ZmOrganizer.TAG && msg.hasTag(organizer.id))) { 909 msg.isUnread = on; 910 flaggedItems.push(msg); 911 } 912 } 913 ZmModel.notifyEach(flaggedItems, ZmEvent.E_FLAGS, {flags:[flag]}); 914 } 915 } 916 } else { 917 ZmList.prototype._folderTreeChangeListener.call(this, ev); 918 } 919 }; 920 921 ZmMailList.prototype._tagTreeChangeListener = 922 function(ev) { 923 if (this.size() == 0) return; 924 925 var flag = ev.getDetail("flag"); 926 if (ev.event == ZmEvent.E_FLAGS && (flag == ZmItem.FLAG_UNREAD)) { 927 this._folderTreeChangeListener(ev); 928 } else { 929 ZmList.prototype._tagTreeChangeListener.call(this, ev); 930 } 931 }; 932