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, 2015, 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, 2015, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * Creates a conversation. 26 * @constructor 27 * @class 28 * This class represents a conversation, which is a collection of mail messages 29 * which have the same subject. 30 * 31 * @param {int} id a unique ID 32 * @param {ZmMailList} list a list that contains this conversation 33 * 34 * @extends ZmMailItem 35 */ 36 ZmConv = function(id, list) { 37 38 ZmMailItem.call(this, ZmItem.CONV, id, list); 39 40 // conversations are always sorted by date desc initially 41 this._sortBy = ZmSearch.DATE_DESC; 42 this._listChangeListener = new AjxListener(this, this._msgListChangeListener); 43 this.folders = {}; 44 this.msgFolder = {}; 45 }; 46 47 ZmConv.prototype = new ZmMailItem; 48 ZmConv.prototype.constructor = ZmConv; 49 50 ZmConv.prototype.isZmConv = true; 51 ZmConv.prototype.toString = function() { return "ZmConv"; }; 52 53 // Public methods 54 55 /** 56 * Creates a conv from its JSON representation. 57 * 58 * @param {Object} node the node 59 * @param {Hash} args a hash of arguments 60 * @return {ZmConv} the conversation 61 */ 62 ZmConv.createFromDom = 63 function(node, args) { 64 var conv = new ZmConv(node.id, args.list); 65 conv._loadFromDom(node); 66 return conv; 67 }; 68 69 /** 70 * Creates a conv from msg data. 71 * 72 * @param {ZmMailMsg} msg the message 73 * @param {Hash} args a hash of arguments 74 * @return {ZmConv} the conversation 75 */ 76 ZmConv.createFromMsg = 77 function(msg, args) { 78 var conv = new ZmConv(msg.cid, args.list); 79 conv._loadFromMsg(msg); 80 return conv; 81 }; 82 83 /** 84 * Ensures that the requested range of msgs is loaded, getting them from the server if needed. 85 * Because the list of msgs returned by the server contains info about which msgs matched the 86 * search, we need to be careful about caching those msgs within the conv. This load function 87 * should be used when in a search context, for example when expanding a conv that is the result 88 * of a search. 89 * 90 * @param {Hash} params a hash of parameters: 91 * @param {String} params.query the query used to retrieve this conv 92 * @param {constant} params.sortBy the sort constraint 93 * @param {int} params.offset the position of first msg to return 94 * @param {int} params.limit the number of msgs to return 95 * @param {Boolean} params.getHtml if <code>true</code>, return HTML part for inlined msg 96 * @param {String} params.fetch which msg bodies to fetch (see soap.txt under SearchConvRequest) 97 * @param {Boolean} params.markRead if <code>true</code>, mark that msg read 98 * @param {boolean} params.needExp if not <code>false</code>, have server check if addresses are DLs 99 * @param {AjxCallback} callback the callback to run with results 100 */ 101 ZmConv.prototype.load = 102 function(params, callback) { 103 104 params = params || {}; 105 var ctlr = appCtxt.getCurrentController(); 106 var query = params.query; 107 if (!query) { 108 query = (ctlr && ctlr.getSearchString) 109 ? ctlr.getSearchString() 110 : appCtxt.get(ZmSetting.INITIAL_SEARCH); 111 } 112 var queryHint = params.queryHint; 113 if (!queryHint) { 114 queryHint = (ctlr && ctlr.getSearchStringHint) 115 ? ctlr.getSearchStringHint() : ""; 116 } 117 var sortBy = params.sortBy || ZmSearch.DATE_DESC; 118 var offset = params.offset || 0; 119 var limit = params.limit || appCtxt.get(ZmSetting.CONVERSATION_PAGE_SIZE); 120 121 var doSearch = true; 122 if (this._loaded && this._expanded && this.msgs && this.msgs.size() && !params.forceLoad) { 123 var size = this.msgs.size(); 124 if (this._sortBy != sortBy || this._query != query || (size != this.numMsgs && !offset)) { 125 this.msgs.clear(); 126 } else if (!this.msgs.hasMore() || offset + limit <= size) { 127 doSearch = false; // we can use cached msg list 128 } 129 } 130 if (!doSearch) { 131 if (callback) { 132 callback.run(this._createResult()); 133 } 134 } else { 135 this._sortBy = sortBy; 136 this._query = query; 137 this._offset = offset; 138 this._limit = limit; 139 140 var searchParams = { 141 query: query, 142 queryHint: queryHint, 143 types: (AjxVector.fromArray([ZmItem.MSG])), 144 sortBy: sortBy, 145 offset: offset, 146 limit: limit, 147 getHtml: (params.getHtml || this.isDraft || appCtxt.get(ZmSetting.VIEW_AS_HTML)), 148 accountName: (appCtxt.multiAccounts && this.getAccount().name) 149 }; 150 151 var search = this.search = new ZmSearch(searchParams), 152 fetch = (params.fetch === true) ? ZmSetting.CONV_FETCH_UNREAD_OR_FIRST : params.fetch || ZmSetting.CONV_FETCH_NONE; 153 154 var needExp = fetch !== ZmSetting.CONV_FETCH_NONE; 155 var convParams = { 156 cid: this.id, 157 callback: (new AjxCallback(this, this._handleResponseLoad, [params, callback, needExp])), 158 fetch: fetch, 159 markRead: params.markRead, 160 noTruncate: params.noTruncate, 161 needExp: needExp 162 }; 163 search.getConv(convParams); 164 } 165 }; 166 167 ZmConv.prototype._handleResponseLoad = 168 function(params, callback, expanded, result) { 169 var results = result.getResponse(); 170 if (!params.offset) { 171 this.msgs = results.getResults(ZmItem.MSG); 172 this.msgs.convId = this.id; 173 this.msgs.addChangeListener(this._listChangeListener); 174 this.msgs.setHasMore(results.getAttribute("more")); 175 this._loaded = true; 176 this._expanded = expanded; 177 } 178 if (callback) { 179 callback.run(result); 180 } 181 }; 182 183 /** 184 * This method supports ZmZimletBase::getMsgsForConv. It loads *all* of this conv's 185 * messages, including their content. Note that it is not search-based, and uses 186 * GetConvRequest rather than SearchConvRequest. 187 * 188 * @param {Hash} params a hash of parameters 189 * @param {Boolean} params.fetchAll if <code>true</code>, fetch content of all msgs 190 * @param {AjxCallback} callback the callback 191 * @param {ZmBatchCommand} batchCmd the batch cmd that contains this request 192 */ 193 ZmConv.prototype.loadMsgs = 194 function(params, callback, batchCmd) { 195 196 params = params || {}; 197 var jsonObj = {GetConvRequest:{_jsns:"urn:zimbraMail"}}; 198 var request = jsonObj.GetConvRequest; 199 var c = request.c = { 200 id: this.id, 201 needExp: true, 202 html: (params.getHtml || this.isDraft || appCtxt.get(ZmSetting.VIEW_AS_HTML)) 203 }; 204 if (params.fetchAll) { 205 c.fetch = "all"; 206 } 207 ZmMailMsg.addRequestHeaders(c); 208 209 // never pass "undefined" as arg to a callback! 210 var respCallback = this._handleResponseLoadMsgs.bind(this, callback || null); 211 if (batchCmd) { 212 batchCmd.addRequestParams(jsonObj, respCallback); 213 } else { 214 appCtxt.getAppController().sendRequest({jsonObj:jsonObj, asyncMode:true, callback:respCallback}); 215 } 216 }; 217 218 ZmConv.prototype._handleResponseLoadMsgs = 219 function(callback, result) { 220 221 var resp = result.getResponse().GetConvResponse.c[0]; 222 this.msgIds = []; 223 224 if (!this.msgs) { 225 // create new msg list 226 this.msgs = new ZmMailList(ZmItem.MSG, this.search); 227 this.msgs.convId = this.id; 228 this.msgs.addChangeListener(this._listChangeListener); 229 } 230 else { 231 //don't recreate if it already exists, so we don't lose listeners.. (see ZmConvView2.prototype.set) 232 this.msgs.removeAllItems(); 233 } 234 if (this.search && !this.msgs.search) { 235 this.msgs.search = this.search; 236 } 237 this.msgs.setHasMore(false); 238 this._loaded = true; 239 240 var len = resp.m.length; 241 //going from last to first since GetConvRequest returns the msgs in order of creation (older first) but we keep things newer first. 242 for (var i = len - 1; i >= 0; i--) { 243 var msgNode = resp.m[i]; 244 this.msgIds.push(msgNode.id); 245 this.msgFolder[msgNode.id] = msgNode.l; 246 msgNode.su = resp.su; 247 // construct ZmMailMsg's so they get cached 248 var msg = ZmMailMsg.createFromDom(msgNode, {list: this.msgs}); 249 this.msgs.add(msg); 250 } 251 252 if (callback) { callback.run(result); } 253 }; 254 255 /** 256 * Adds the message at the given index. 257 * 258 * @param {ZmMailMsg} msg the message to add 259 * @param {int} index where to add it 260 */ 261 ZmConv.prototype.addMsg = 262 function(msg, index) { 263 264 if (!this.msgs) { 265 this.msgs = new ZmMailList(ZmItem.MSG, this.search); 266 this.msgs.convId = this.id; 267 this.msgs.addChangeListener(this._listChangeListener); 268 this.msgs.setHasMore(false); 269 } 270 if (this.search && !this.msgs.search) { 271 this.msgs.search = this.search; 272 } 273 this.msgs.add(msg, index); 274 this.msgIds = []; 275 var a = this.msgs.getArray(); 276 for (var i = 0, len = a.length; i < len; i++) { 277 this.msgIds.push(a[i].id); 278 } 279 this.msgFolder[msg.id] = msg.folderId; 280 }; 281 282 /** 283 * Removes the message. 284 * 285 * @param {ZmMailMsg} msg the message to remove 286 */ 287 ZmConv.prototype.removeMsg = 288 function(msg) { 289 if (this.msgs) { 290 this.msgs.remove(msg, true); 291 } 292 if (this.msgIds && this.msgIds.length) { 293 AjxUtil.arrayRemove(this.msgIds, msg.id); 294 } 295 delete this.msgFolder[msg.id]; 296 }; 297 298 ZmConv.prototype.canAddTag = 299 function(tagName) { 300 if (!this.msgs) { 301 return ZmItem.prototype.canAddTag.call(this, tagName); 302 } 303 var msgs = this.msgs.getArray(); 304 for (var i = 0; i < msgs.length; i++) { 305 var msg = msgs[i]; 306 if (msg.canAddTag(tagName)) { 307 return true; 308 } 309 } 310 return false; 311 }; 312 313 ZmConv.prototype.mute = 314 function() { 315 this.isMute = true; 316 if(this.msgs) { 317 var msgs = this.msgs.getArray(); 318 for (var i = 0; i < msgs.length; i++) { 319 var msg = msgs[i]; 320 msg.mute(); 321 } 322 } 323 }; 324 325 ZmConv.prototype.unmute = 326 function() { 327 this.isMute = false; 328 if(this.msgs) { 329 var msgs = this.msgs.getArray(); 330 for (var i = 0; i < msgs.length; i++) { 331 var msg = msgs[i]; 332 msg.unmute(); 333 } 334 } 335 }; 336 337 /** 338 * Gets the mute/unmute icon. 339 * 340 * @return {String} the icon 341 */ 342 ZmConv.prototype.getMuteIcon = 343 function() { 344 return this.isMute ? "Mute" : "Unmute"; 345 }; 346 347 348 ZmConv.prototype.clear = 349 function() { 350 if (this.isInUse) { 351 return; 352 } 353 if (this.msgs) { 354 this.msgs.clear(); 355 this.msgs.removeChangeListener(this._listChangeListener); 356 this.msgs = null; 357 } 358 this.msgIds = []; 359 this.folders = {}; 360 this.msgFolder = {}; 361 362 ZmMailItem.prototype.clear.call(this); 363 }; 364 365 /** 366 * Checks if the conversation is read only. Returns false if it cannot be determined. 367 * 368 * @return {Boolean} <code>true</code> if the conversation is read only 369 */ 370 ZmConv.prototype.isReadOnly = 371 function() { 372 373 if (this._loaded && this.msgs && this.msgs.size()) { 374 // conv has been loaded, check each msg 375 var msgs = this.msgs.getArray(); 376 for (var i = 0; i < msgs.length; i++) { 377 if (msgs[i].isReadOnly()) { 378 return true; 379 } 380 } 381 } 382 else { 383 // conv has not been loaded, see if it's constrained to a folder 384 var folderId = this.getFolderId(); 385 var folder = folderId && appCtxt.getById(folderId); 386 return !!(folder && folder.isReadOnly()); 387 } 388 return false; 389 }; 390 391 /** 392 * Checks if this conversation has a message that matches the given search. 393 * If we're not able to tell whether a msg matches, we return the given default value. 394 * 395 * @param {ZmSearch} search the search to match against 396 * @param {Object} defaultValue the value to return if search is not matchable or conv not loaded 397 * @return {Boolean} <code>true</code> if this conversation has a matching message 398 */ 399 ZmConv.prototype.hasMatchingMsg = 400 function(search, defaultValue) { 401 402 var msgs = this.msgs && this.msgs.getArray(), 403 hasUnknown = false; 404 405 if (msgs && msgs.length > 0) { 406 for (var i = 0; i < msgs.length; i++) { 407 var msg = msgs[i], 408 msgMatches = search.matches(msg); 409 410 if (msgMatches && !msg.ignoreJunkTrash() && this.folders[msg.folderId]) { 411 return true; 412 } 413 else if (msgMatches === null) { 414 hasUnknown = true; 415 } 416 } 417 } 418 419 return hasUnknown ? !!defaultValue : false; 420 }; 421 422 ZmConv.prototype.containsMsg = 423 function(msg) { 424 return this.msgIds && AjxUtil.arrayContains(this.msgIds, msg.id); 425 }; 426 427 ZmConv.prototype.ignoreJunkTrash = 428 function() { 429 return Boolean((this.numMsgs == 1) && this.folders && 430 ((this.folders[ZmFolder.ID_SPAM] && !appCtxt.get(ZmSetting.SEARCH_INCLUDES_SPAM)) || 431 (this.folders[ZmFolder.ID_TRASH] && !appCtxt.get(ZmSetting.SEARCH_INCLUDES_TRASH)))); 432 }; 433 434 ZmConv.prototype.getAccount = 435 function() { 436 // pull out the account from the fully-qualified ID 437 if (!this.account) { 438 var folderId = this.getFolderId(); 439 var folder = folderId && appCtxt.getById(folderId); 440 // make sure current folder is not remote folder 441 // in that case getting account from parseID will fail if 442 // the shared account is also configured in ZD 443 if (!(folder && folder._isRemote)) { 444 this.account = ZmOrganizer.parseId(this.id).account; 445 } 446 } 447 448 // fallback on the active account if account not found from parsed ID (most 449 // likely means this is a conv inside a shared folder of the active acct) 450 if (!this.account) { 451 this.account = appCtxt.getActiveAccount(); 452 } 453 return this.account; 454 455 }; 456 457 /** 458 * Handles a modification notification. 459 * TODO: Bundle MODIFY notifs (should bubble up to parent handlers as well) 460 * 461 * @param obj item with the changed attributes/content 462 * 463 * @private 464 */ 465 ZmConv.prototype.notifyModify = 466 function(obj, batchMode) { 467 var fields = {}; 468 // a conv's ID can change if it's a virtual conv becoming real; 'this' will be 469 // the old conv; if we can, we switch to using the new conv, which will be more 470 // up to date; the new conv will be available if it was received via search results 471 if (obj._newId != null) { 472 var conv = appCtxt.getById(obj._newId) || this; 473 conv._oldId = this.id; 474 conv.id = obj._newId; 475 appCtxt.cacheSet(conv._oldId); 476 appCtxt.cacheSet(conv.id, conv); 477 conv.msgs = conv.msgs || this.msgs; 478 if (conv.msgs) { 479 conv.msgs.convId = conv.id; 480 var a = conv.msgs.getArray(); 481 for (var i = 0; i < a.length; i++) { 482 a[i].cid = conv.id; 483 } 484 } 485 conv.folders = AjxUtil.hashCopy(this.folders); 486 if (conv.list && conv._oldId && conv.list._idHash[conv._oldId]) { 487 delete conv.list._idHash[conv._oldId]; 488 conv.list._idHash[conv.id] = conv; 489 } 490 fields[ZmItem.F_ID] = true; 491 conv._notify(ZmEvent.E_MODIFY, {fields : fields}); 492 } 493 if (obj.n != null) { 494 this.numMsgs = obj.n; 495 fields[ZmItem.F_SIZE] = true; 496 this._notify(ZmEvent.E_MODIFY, {fields : fields}); 497 } 498 499 return ZmMailItem.prototype.notifyModify.apply(this, arguments); 500 }; 501 502 /** 503 * Checks if any of the msgs within this conversation has the given value for 504 * the given flag. If the conv hasn't been loaded, looks at the conv-level flag. 505 * 506 * @param {constant} flag the flag (see <code>ZmItem.FLAG_</code> constants) 507 * @param {Boolean} value the test value 508 * @return {Boolean} <code>true</code> if the flag exists 509 */ 510 ZmConv.prototype.hasFlag = 511 function(flag, value) { 512 if (!this.msgs) { 513 return (this[ZmItem.FLAG_PROP[flag]] == value); 514 } 515 var msgs = this.msgs.getArray(); 516 for (var j = 0; j < msgs.length; j++) { 517 var msg = msgs[j]; 518 if (msg[ZmItem.FLAG_PROP[flag]] == value) { 519 return true; 520 } 521 } 522 return false; 523 }; 524 525 /** 526 * Checks to see if a change in the value of a msg flag changes the value of the conv's flag. That will happen 527 * for the first msg to get an off flag turned on, or when the last msg to have an on flag turns it off. 528 */ 529 ZmConv.prototype._checkFlags = 530 function(flags) { 531 532 var convOn = {}; 533 var msgsOn = {}; 534 for (var i = 0; i < flags.length; i++) { 535 var flag = flags[i]; 536 if (!(flag == ZmItem.FLAG_FLAGGED || flag == ZmItem.FLAG_UNREAD || flag == ZmItem.FLAG_MUTE || flag == ZmItem.FLAG_ATTACH || flag == ZmItem.FLAG_PRIORITY)) { continue; } 537 convOn[flag] = this[ZmItem.FLAG_PROP[flag]]; 538 msgsOn[flag] = this.hasFlag(flag, true); 539 } 540 var doNotify = false; 541 var flags = []; 542 for (var flag in convOn) { 543 if (convOn[flag] != msgsOn[flag]) { 544 this[ZmItem.FLAG_PROP[flag]] = msgsOn[flag]; 545 flags.push(flag); 546 doNotify = true; 547 } 548 } 549 550 if (doNotify) { 551 this._notify(ZmEvent.E_FLAGS, {flags: flags}); 552 } 553 }; 554 555 /** 556 * Figure out if any tags have been added or removed by comparing what we have now with what 557 * our messages have. 558 * 559 * @private 560 */ 561 ZmConv.prototype._checkTags = 562 function() { 563 var newTags = {}; 564 var allTags = {}; 565 566 for (var tagId in this.tagHash) { 567 allTags[tagId] = true; 568 } 569 570 if (this.msgs) { 571 var msgs = this.msgs.getArray(); 572 if (!(msgs && msgs.length)) { return; } 573 for (var i = 0; i < msgs.length; i++) { 574 for (var tagId in msgs[i].tagHash) { 575 newTags[tagId] = true; 576 allTags[tagId] = true; 577 } 578 } 579 580 var notify = false; 581 for (var tagId in allTags) { 582 if (!this.tagHash[tagId] && newTags[tagId]) { 583 if (this.tagLocal(tagId, true)) { 584 notify = true; 585 } 586 } else if (this.tagHash[tagId] && !newTags[tagId]) { 587 if (this.tagLocal(tagId, false)) { 588 notify = true; 589 } 590 } 591 } 592 } 593 594 if (notify) { 595 this._notify(ZmEvent.E_TAGS); 596 } 597 }; 598 599 ZmConv.prototype.moveLocal = 600 function(folderId) { 601 if (this.folders) { 602 delete this.folders; 603 } 604 this.folders = {}; 605 this.folders[folderId] = true; 606 }; 607 608 ZmConv.prototype.getMsgList = 609 function(offset, ascending, omit) { 610 // this.msgs will not be set if the conv has not yet been loaded 611 var list = this.msgs && this.msgs.getArray(); 612 var a = list ? (list.slice(offset || 0)) : []; 613 if (omit) { 614 var a1 = []; 615 for (var i = 0; i < a.length; i++) { 616 var msg = a[i]; 617 if (!(msg && msg.folderId && omit[msg.folderId])) { 618 a1.push(msg); 619 } 620 } 621 a = a1; 622 } 623 if (ascending) { 624 a.reverse(); 625 } 626 return a; 627 }; 628 629 ZmConv.prototype.getFolderId = 630 function() { 631 return this.folderId || (this.list && this.list.search && this.list.search.folderId); 632 }; 633 634 /** 635 * Gets the first relevant msg of this conv, loading the conv msg list if necessary. If the 636 * msg itself hasn't been loaded we also load the conv. The conv load is a SearchConvRequest 637 * which fetches the content of the first msg and returns it via a callback. If no 638 * callback is provided, the conv will not be loaded - if it already has a msg list, the msg 639 * will come from there; otherwise, a skeletal msg with an ID is returned. Note that a conv 640 * always has at least one msg. 641 * 642 * @param {Hash} params a hash of parameters 643 * @param {String} params.query the query used to retrieve this conv 644 * @param {constant} params.sortBy the sort constraint 645 * @param {int} params.offset the position of first msg to return 646 * @param {int} params.limit the number of msgs to return 647 * @param {AjxCallback} callback the callback to run with results 648 * 649 * @return {ZmMailMsg} the message 650 */ 651 ZmConv.prototype.getFirstHotMsg = 652 function(params, callback) { 653 654 var msg; 655 params = params || {}; 656 657 if (this.msgs && this.msgs.size()) { 658 msg = this.msgs.getFirstHit(params.offset, params.limit, params.foldersToOmit); 659 } 660 661 if (callback) { 662 if (msg && msg._loaded && !params.forceLoad) { 663 callback.run(msg); 664 } 665 else { 666 var respCallback = this._handleResponseGetFirstHotMsg.bind(this, params, callback); 667 params.fetch = ZmSetting.CONV_FETCH_FIRST; 668 this.load(params, respCallback); 669 } 670 } 671 else { 672 // do our best to return a "realized" message by checking cache 673 if (!msg && this.msgIds && this.msgIds.length) { 674 var id = this.msgIds[0]; 675 msg = appCtxt.getById(id); 676 if (!msg) { 677 if (!this.msgs) { 678 this.msgs = new ZmMailList(ZmItem.MSG); 679 this.msgs.convId = this.id; 680 this.msgs.addChangeListener(this._listChangeListener); 681 } 682 msg = new ZmMailMsg(id, this.msgs); 683 } 684 } 685 return msg; 686 } 687 }; 688 689 ZmConv.prototype._handleResponseGetFirstHotMsg = function(params, callback) { 690 691 var msg = this.msgs.getFirstHit(params.offset, params.limit, params.foldersToOmit); 692 // should have a loaded msg 693 if (msg && msg._loaded) { 694 if (callback) { 695 callback.run(msg); 696 } 697 } 698 else { 699 // desperate measures - get msg content from server 700 if (!msg && this.msgIds && this.msgIds.length) { 701 msg = new ZmMailMsg(this.msgIds[0]); 702 } 703 var respCallback = this._handleResponseLoadMsg.bind(this, msg, callback); 704 msg.load({getHtml:params.getHtml, callback:respCallback}); 705 } 706 }; 707 708 ZmConv.prototype._handleResponseLoadMsg = 709 function(msg, callback) { 710 if (msg && callback) { 711 callback.run(msg); 712 } 713 }; 714 715 ZmConv.prototype._loadFromDom = 716 function(convNode) { 717 718 this.numMsgs = convNode.n; 719 this.date = convNode.d; 720 this._parseFlagsOfMsgs(convNode.m); // parse flags based on msgs 721 this._parseTagNames(convNode.tn); 722 if (convNode.e) { 723 for (var i = 0; i < convNode.e.length; i++) { 724 this._parseParticipantNode(convNode.e[i]); 725 } 726 } 727 this.participantsElided = convNode.elided; 728 this.subject = convNode.su; 729 this.fragment = convNode.fr; 730 this.sf = convNode.sf; 731 732 // note that the list of msg IDs in a search result is partial - only msgs that matched are included 733 if (convNode.m) { 734 this.msgIds = []; 735 this.msgFolder = {}; 736 for (var i = 0, count = convNode.m.length; i < count; i++) { 737 var msgNode = convNode.m[i]; 738 this.msgIds.push(msgNode.id); 739 this.msgFolder[msgNode.id] = msgNode.l; 740 this.folders[msgNode.l] = true; 741 } 742 if (count == 1) { 743 var msgNode = convNode.m[0]; 744 745 // bug 49067 - SearchConvResponse does not return the folder ID w/in 746 // the msgNode as fully qualified so reset if this 1-msg conv was 747 // returned by a simple folder search 748 // TODO: if 85358 is fixed, we can remove this section 749 var searchFolderId = this.list && this.list.search && this.list.search.folderId; 750 if (searchFolderId) { 751 this.folderId = searchFolderId; 752 this.folders[searchFolderId] = true; 753 } else if (msgNode.l) { 754 this.folderId = msgNode.l; 755 this.folders[msgNode.l] = true; 756 } 757 else { 758 AjxDebug.println(AjxDebug.NOTIFY, "no folder added for conv"); 759 } 760 if (msgNode.s) { 761 this.size = msgNode.s; 762 } 763 764 if (msgNode.autoSendTime) { 765 var timestamp = parseInt(msgNode.autoSendTime); 766 if (timestamp) { 767 this.setAutoSendTime(new Date(timestamp)); 768 } 769 } 770 } 771 } 772 773 // Grab the metadata, keyed off the section name 774 if (convNode.meta) { 775 this.meta = {}; 776 for (var i = 0; i < convNode.meta.length; i++) { 777 var section = convNode.meta[i].section; 778 this.meta[section] = {}; 779 this.meta[section]._attrs = {}; 780 for (a in convNode.meta[i]._attrs) { 781 this.meta[section]._attrs[a] = convNode.meta[i]._attrs[a]; 782 } 783 } 784 } 785 }; 786 787 ZmConv.prototype._loadFromMsg = 788 function(msg) { 789 this.date = msg.date; 790 this.isFlagged = msg.isFlagged; 791 this.isUnread = msg.isUnread; 792 for (var i = 0; i < msg.tags.length; i++) { 793 this.tagLocal(msg.tags[i], true); 794 } 795 var a = msg.participants ? msg.participants.getArray() : null; 796 this.participants = new AjxVector(); 797 if (a && a.length) { 798 for (var i = 0; i < a.length; i++) { 799 var p = a[i]; 800 if ((msg.isDraft && p.type == AjxEmailAddress.TO) || 801 (!msg.isDraft && p.type == AjxEmailAddress.FROM)) { 802 this.participants.add(p); 803 } 804 } 805 } 806 this.subject = msg.subject; 807 this.fragment = msg.fragment; 808 this.sf = msg.sf; 809 this.msgIds = [msg.id]; 810 this.msgFolder[msg.id] = msg.folderId; 811 //add a flag to redraw this conversation when additional information is available 812 this.redrawConvRow = true; 813 }; 814 815 ZmConv.prototype._msgListChangeListener = 816 function(ev) { 817 if (ev.type != ZmEvent.S_MSG) { return; } 818 if (ev.event == ZmEvent.E_TAGS || ev.event == ZmEvent.E_REMOVE_ALL) { 819 this._checkTags(); 820 } else if (ev.event == ZmEvent.E_FLAGS) { 821 this._checkFlags(ev.getDetail("flags")); 822 } else if (ev.event == ZmEvent.E_DELETE || ev.event == ZmEvent.E_MOVE) { 823 // a msg was moved or deleted, see if this conv's row should remain 824 if (this.list && this.list.search && !this.hasMatchingMsg(this.list.search, true)) { 825 this.moveLocal(ev.item && ev.item.folderId); 826 this._notify(ev.event); 827 } 828 } 829 }; 830 831 /** 832 * Returns a result created from this conv's data that looks as if it were the result 833 * of an actual SOAP request. 834 * 835 * @private 836 */ 837 ZmConv.prototype._createResult = 838 function() { 839 var searchResult = new ZmSearchResult(this.search); 840 searchResult.type = ZmItem.MSG; 841 searchResult._results[ZmItem.MSG] = this.msgs; 842 return new ZmCsfeResult(searchResult); 843 }; 844 845 // Updates the conversation fragment based on the newest message in the conversation, optionally ignoring an array of messages 846 ZmConv.prototype.updateFragment = 847 function(ignore) { 848 var best; 849 var size = this.msgs && this.msgs.size(); 850 if (size) { 851 for (var j = size - 1; j >= 0; j--) { 852 var candidate = this.msgs.get(j); 853 if (ignore && AjxUtil.indexOf(ignore, candidate) != -1) { continue; } 854 if (candidate.fragment && (!best || candidate.date > best.date)) { 855 best = candidate; 856 } 857 } 858 } 859 if (best) { 860 this.fragment = best.fragment; 861 } 862 }; 863 864 /** 865 * Gets a vector of addresses of the given type. 866 * 867 * @param {constant} type an email address type 868 * 869 * @return {AjxVector} a vector of email addresses 870 */ 871 ZmConv.prototype.getAddresses = 872 function(type) { 873 874 var p = this.participants ? this.participants.getArray() : []; 875 var list = []; 876 for (var i = 0, len = p.length; i < len; i++) { 877 var addr = p[i]; 878 if (addr.type == type) { 879 list.push(addr); 880 } 881 } 882 return AjxVector.fromArray(list); 883 }; 884 885 /** 886 * Gets the status tool tip. 887 * 888 * @return {String} the tool tip 889 */ 890 ZmConv.prototype.getStatusTooltip = 891 function() { 892 if (this.numMsgs === 1 && this.msgIds && this.msgIds.length > 0) { 893 var msg = appCtxt.getById(this.msgIds[0]); 894 if (msg) { 895 return msg.getStatusTooltip(); 896 } 897 } 898 899 var status = []; 900 901 // keep in sync with ZmMailMsg.prototype.getStatusTooltip 902 if (this.isScheduled) { 903 status.push(ZmMsg.scheduled); 904 } 905 if (this.isUnread) { 906 status.push(ZmMsg.unread); 907 } 908 if (this.isReplied) { 909 status.push(ZmMsg.replied); 910 } 911 if (this.isForwarded) { 912 status.push(ZmMsg.forwarded); 913 } 914 if (this.isDraft) { 915 status.push(ZmMsg.draft); 916 } else if (this.isSent) { 917 //sentAt is for some reason "sent", which is what we need. 918 status.push(ZmMsg.sentAt); 919 } 920 921 return status.join(", "); 922 }; 923 924 /** 925 * Returns the number of unread messages in this conversation. 926 */ 927 ZmConv.prototype.getNumUnreadMsgs = 928 function() { 929 var numUnread = 0; 930 var msgs = this.getMsgList(); 931 if (msgs) { 932 for (var i = 0, len = msgs.length; i < len; i++) { 933 if (msgs[i].isUnread) { 934 numUnread++; 935 } 936 } 937 return numUnread; 938 } 939 return null; 940 }; 941 942 /** 943 * Parse flags based on which flags are in the messages we will display (which normally 944 * excludes messages in Trash or Junk). 945 * 946 * @param [array] msgs msg nodes from search result 947 * 948 * @private 949 */ 950 ZmConv.prototype._parseFlagsOfMsgs = function(msgs) { 951 952 // use search from list since it's not yet set in controller 953 var ignore = ZmMailApp.getFoldersToOmit(this.list && this.list.search), 954 msg, len = msgs ? msgs.length : 0, i, 955 flags = {}; 956 957 for (i = 0; i < len; i++) { 958 msg = msgs[i]; 959 if (!ignore[msg.l]) { 960 var msgFlags = msg.f && msg.f.split(''), 961 len1 = msgFlags ? msgFlags.length : 0, j; 962 963 for (j = 0; j < len1; j++) { 964 flags[msgFlags[j]] = true; 965 } 966 } 967 } 968 969 this.flags = AjxUtil.keys(flags).join(''); 970 ZmItem.prototype._parseFlags.call(this, this.flags); 971 }; 972