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 * @overview 26 * The file defines a search class. 27 * 28 */ 29 30 /** 31 * Creates a new search with the given properties. 32 * @class 33 * This class represents a search to be performed on the server. It has properties for 34 * the different search parameters that may be used. It can be used for a regular search, 35 * or to search within a conversation. The results are returned via a callback. 36 * 37 * @param {Hash} params a hash of parameters 38 * @param {String} params.query the query string 39 * @param {String} params.queryHint the query string that gets appended to the query but not something the user needs to know about 40 * @param {AjxVector} params.types the item types to search for 41 * @param {Boolean} params.forceTypes use the types we pass, do not override (in case of mail) to the current user's view pref (MSG vs. CONV). 42 * @param {constant} params.sortBy the sort order 43 * @param {int} params.offset the starting point within result set 44 * @param {int} params.limit the number of results to return 45 * @param {Boolean} params.getHtml if <code>true</code>, return HTML part for inlined msg 46 * @param {constant} params.contactSource where to search for contacts (GAL or personal) 47 * @param {Boolean} params.isGalAutocompleteSearch if <code>true</code>, autocomplete against GAL 48 * @param {constant} params.galType the type of GAL autocomplete (account or resource) 49 * @param {constant} params.autocompleteType the type of autocomplete (account or resource or all) 50 * @param {int} params.lastId the ID of last item displayed (for pagination) 51 * @param {String} params.lastSortVal the value of sort field for above item 52 * @param {Boolean} params.fetch if <code>true</code>, fetch first hit message 53 * @param {int} params.searchId the ID of owning search folder (if any) 54 * @param {Array} params.conds the list of search conditions (<code><SearchCalendarResourcesRequest></code>) 55 * @param {Array} params.attrs the list of attributes to return (<code><SearchCalendarResourcesRequest></code>) 56 * @param {String} params.field the field to search within (instead of default) 57 * @param {Object} params.soapInfo the object with method, namespace, response, and additional attribute fields for creating soap doc 58 * @param {Object} params.response the canned JSON response (no request will be made) 59 * @param {Array} params.folders the list of folders for autocomplete 60 * @param {Array} params.allowableTaskStatus the list of task status types to return (assuming one of the values for "types" is "task") 61 * @param {String} params.accountName the account name to run this search against 62 * @param {Boolean} params.idsOnly if <code>true</code>, response returns item IDs only 63 * @param {Boolean} params.inDumpster if <code>true</code>, search in the dumpster 64 * @param {string} params.origin indicates what initiated the search 65 * @param {boolean} params.isEmpty if true, return empty response without sending a request 66 */ 67 ZmSearch = function(params) { 68 69 params = params || {}; 70 for (var p in params) { 71 this[p] = params[p]; 72 } 73 this.galType = this.galType || ZmSearch.GAL_ACCOUNT; 74 this.join = this.join || ZmSearch.JOIN_AND; 75 76 if (this.query || this.queryHint) { 77 // only parse regular searches 78 if (!this.isGalSearch && !this.isAutocompleteSearch && 79 !this.isGalAutocompleteSearch && !this.isCalResSearch) { 80 81 var pq = this.parsedQuery = new ZmParsedQuery(this.query || this.queryHint); 82 this._setProperties(); 83 var sortTerm = pq.getTerm("sort"); 84 if (sortTerm) { 85 this.sortBy = sortTerm.arg; 86 } 87 } 88 } 89 90 this.isGalSearch = false; 91 this.isCalResSearch = false; 92 93 if (ZmSearch._mailEnabled == null) { 94 ZmSearch._mailEnabled = appCtxt.get(ZmSetting.MAIL_ENABLED); 95 if (ZmSearch._mailEnabled) { 96 AjxDispatcher.require("MailCore"); 97 } 98 } 99 100 if (params.checkTypes) { 101 var types = AjxUtil.toArray(this.types); 102 var enabledTypes = []; 103 for (var i = 0; i < types.length; i++) { 104 var type = types[i]; 105 var app = ZmItem.APP[type]; 106 if (appCtxt.get(ZmApp.SETTING[app])) { 107 enabledTypes.push(type); 108 } 109 } 110 this.types = AjxVector.fromArray(enabledTypes); 111 } 112 }; 113 114 ZmSearch.prototype.isZmSearch = true; 115 ZmSearch.prototype.toString = function() { return "ZmSearch"; }; 116 117 // Search types 118 ZmSearch.TYPE = {}; 119 ZmSearch.TYPE_ANY = "any"; 120 121 ZmSearch.GAL_ACCOUNT = "account"; 122 ZmSearch.GAL_RESOURCE = "resource"; 123 ZmSearch.GAL_ALL = ""; 124 125 ZmSearch.JOIN_AND = 1; 126 ZmSearch.JOIN_OR = 2; 127 128 ZmSearch.TYPE_MAP = {}; 129 130 ZmSearch.DEFAULT_LIMIT = DwtListView.DEFAULT_LIMIT; 131 132 // Sort By 133 ZmSearch.DATE_DESC = "dateDesc"; 134 ZmSearch.DATE_ASC = "dateAsc"; 135 ZmSearch.SUBJ_DESC = "subjDesc"; 136 ZmSearch.SUBJ_ASC = "subjAsc"; 137 ZmSearch.NAME_DESC = "nameDesc"; 138 ZmSearch.NAME_ASC = "nameAsc"; 139 ZmSearch.SIZE_DESC = "sizeDesc"; 140 ZmSearch.SIZE_ASC = "sizeAsc"; 141 ZmSearch.RCPT_ASC = "rcptAsc"; 142 ZmSearch.RCPT_DESC = "rcptDesc"; 143 ZmSearch.ATTACH_ASC = "attachAsc" 144 ZmSearch.ATTACH_DESC = "attachDesc" 145 ZmSearch.FLAG_ASC = "flagAsc"; 146 ZmSearch.FLAG_DESC = "flagDesc"; 147 ZmSearch.MUTE_ASC = "muteAsc"; 148 ZmSearch.MUTE_DESC = "muteDesc"; 149 ZmSearch.READ_ASC = "readAsc"; 150 ZmSearch.READ_DESC = "readDesc"; 151 ZmSearch.PRIORITY_ASC = "priorityAsc"; 152 ZmSearch.PRIORITY_DESC = "priorityDesc"; 153 ZmSearch.SCORE_DESC = "scoreDesc"; 154 ZmSearch.DURATION_DESC = "durDesc"; 155 ZmSearch.DURATION_ASC = "durAsc"; 156 ZmSearch.STATUS_DESC = "taskStatusDesc"; 157 ZmSearch.STATUS_ASC = "taskStatusAsc"; 158 ZmSearch.PCOMPLETE_DESC = "taskPercCompletedDesc"; 159 ZmSearch.PCOMPLETE_ASC = "taskPercCompletedAsc"; 160 ZmSearch.DUE_DATE_DESC = "taskDueDesc"; 161 ZmSearch.DUE_DATE_ASC = "taskDueAsc"; 162 163 164 165 ZmSearch.prototype.execute = 166 function(params) { 167 if (params.batchCmd || this.soapInfo) { 168 return this._executeSoap(params); 169 } else { 170 return this._executeJson(params); 171 } 172 }; 173 174 /** 175 * Creates a SOAP request that represents this search and sends it to the server. 176 * 177 * @param {Hash} params a hash of parameters 178 * @param {AjxCallback} params.callback the callback to run when response is received 179 * @param {AjxCallback} params.errorCallback the callback to run if there is an exception 180 * @param {ZmBatchCommand} params.batchCmd the batch command that contains this request 181 * @param {int} params.timeout the timeout value (in seconds) 182 * @param {Boolean} params.noBusyOverlay if <code>true</code>, don't use the busy overlay 183 * 184 * @private 185 */ 186 ZmSearch.prototype._executeSoap = 187 function(params) { 188 189 this.isGalSearch = (this.contactSource && (this.contactSource == ZmId.SEARCH_GAL)); 190 this.isCalResSearch = (!this.contactSource && this.conds != null); 191 if (appCtxt.isOffline && this.isCalResSearch) { 192 this.isCalResSearch = appCtxt.isZDOnline(); 193 } 194 if (this.isEmpty) { 195 this._handleResponseExecute(params.callback); 196 return null; 197 } 198 199 var soapDoc; 200 if (!this.response) { 201 if (this.isGalSearch) { 202 // DEPRECATED: Use JSON version 203 soapDoc = AjxSoapDoc.create("SearchGalRequest", "urn:zimbraAccount"); 204 var method = soapDoc.getMethod(); 205 if (this.galType) { 206 method.setAttribute("type", this.galType); 207 } 208 soapDoc.set("name", this.query); 209 var searchFilterEl = soapDoc.set("searchFilter"); 210 if (this.conds && this.conds.length) { 211 var condsEl = soapDoc.set("conds", null, searchFilterEl); 212 this._applySoapCond(this.conds, soapDoc, condsEl); 213 } 214 } else if (this.isAutocompleteSearch) { 215 soapDoc = AjxSoapDoc.create("AutoCompleteRequest", "urn:zimbraMail"); 216 var method = soapDoc.getMethod(); 217 if (this.limit) { 218 method.setAttribute("limit", this.limit); 219 } 220 soapDoc.set("name", this.query); 221 } else if (this.isGalAutocompleteSearch) { 222 soapDoc = AjxSoapDoc.create("AutoCompleteGalRequest", "urn:zimbraAccount"); 223 var method = soapDoc.getMethod(); 224 method.setAttribute("limit", this._getLimit()); 225 if (this.galType) { 226 method.setAttribute("type", this.galType); 227 } 228 soapDoc.set("name", this.query); 229 } else if (this.isCalResSearch) { 230 soapDoc = AjxSoapDoc.create("SearchCalendarResourcesRequest", "urn:zimbraAccount"); 231 var method = soapDoc.getMethod(); 232 if (this.attrs) { 233 var attrs = [].concat(this.attrs); 234 AjxUtil.arrayRemove(attrs, "fullName"); 235 method.setAttribute("attrs", attrs.join(",")); 236 } 237 var searchFilterEl = soapDoc.set("searchFilter"); 238 if (this.conds && this.conds.length) { 239 var condsEl = soapDoc.set("conds", null, searchFilterEl); 240 this._applySoapCond(this.conds, soapDoc, condsEl); 241 } 242 } else { 243 if (this.soapInfo) { 244 soapDoc = AjxSoapDoc.create(this.soapInfo.method, this.soapInfo.namespace); 245 // Pass along any extra soap data. (Voice searches use this to pass user identification.) 246 for (var nodeName in this.soapInfo.additional) { 247 var node = soapDoc.set(nodeName); 248 var attrs = this.soapInfo.additional[nodeName]; 249 for (var attr in attrs) { 250 node.setAttribute(attr, attrs[attr]); 251 } 252 } 253 } else { 254 soapDoc = AjxSoapDoc.create("SearchRequest", "urn:zimbraMail"); 255 } 256 var method = this._getStandardMethod(soapDoc); 257 if (this.types) { 258 var a = this.types.getArray(); 259 if (a.length) { 260 var typeStr = []; 261 for (var i = 0; i < a.length; i++) { 262 typeStr.push(ZmSearch.TYPE[a[i]]); 263 } 264 method.setAttribute("types", typeStr.join(",")); 265 if (this.types.contains(ZmItem.MSG) || this.types.contains(ZmItem.CONV)) { 266 // special handling for showing participants ("To" instead of "From") 267 var folder = this.folderId && appCtxt.getById(this.folderId); 268 method.setAttribute("recip", (folder && folder.isOutbound()) ? "1" : "0"); 269 } 270 if (this.types.contains(ZmItem.CONV)) { 271 // get ID/folder for every msg in each conv result 272 method.setAttribute("fullConversation", 1); 273 } 274 // if we're prefetching the first hit message, also mark it as read 275 if (this.fetch) { 276 277 method.setAttribute("fetch", ( this.fetch == "all" ) ? "all" : "1"); 278 // and set the html flag if we want the html version 279 if (this.getHtml) { 280 method.setAttribute("html", "1"); 281 } 282 } 283 if (this.markRead) { 284 method.setAttribute("read", "1"); 285 } 286 } 287 } 288 if (this.inDumpster) { 289 method.setAttribute("inDumpster", "1"); 290 } 291 } 292 } 293 294 var soapMethod = this._getStandardMethod(soapDoc); 295 soapMethod.setAttribute("needExp", 1); 296 297 var respCallback = this._handleResponseExecute.bind(this, params.callback); 298 299 if (params.batchCmd) { 300 params.batchCmd.addRequestParams(soapDoc, respCallback); 301 } else { 302 return appCtxt.getAppController().sendRequest({soapDoc:soapDoc, asyncMode:true, callback:respCallback, 303 errorCallback:params.errorCallback, 304 timeout:params.timeout, noBusyOverlay:params.noBusyOverlay, 305 response:this.response}); 306 } 307 }; 308 309 /** 310 * Creates a JSON request that represents this search and sends it to the server. 311 * 312 * @param {Hash} params a hash of parameters 313 * @param {AjxCallback} params.callback the callback to run when response is received 314 * @param {AjxCallback} params.errorCallback the callback to run if there is an exception 315 * @param {ZmBatchCommand} params.batchCmd the batch command that contains this request 316 * @param {int} params.timeout the timeout value (in seconds) 317 * @param {Boolean} params.noBusyOverlay if <code>true</code>, don't use the busy overlay 318 * 319 * @private 320 */ 321 ZmSearch.prototype._executeJson = 322 function(params) { 323 324 this.isGalSearch = (this.contactSource && (this.contactSource == ZmId.SEARCH_GAL)); 325 this.isCalResSearch = (!this.contactSource && this.conds != null); 326 if (appCtxt.isOffline && this.isCalResSearch) { 327 this.isCalResSearch = appCtxt.isZDOnline(); 328 } 329 if (this.isEmpty) { 330 this._handleResponseExecute(params.callback); 331 return null; 332 } 333 334 var jsonObj, request, soapDoc; 335 if (!this.response) { 336 if (this.isGalSearch) { 337 request = { 338 _jsns:"urn:zimbraAccount", 339 needIsOwner: "1", 340 needIsMember: "directOnly" 341 }; 342 jsonObj = {SearchGalRequest: request}; 343 if (this.galType) { 344 request.type = this.galType; 345 } 346 request.name = this.query; 347 348 // bug #36188 - add offset/limit for paging support 349 request.offset = this.offset = (this.offset || 0); 350 request.limit = this._getLimit(); 351 352 // bug 15878: see same in ZmSearch.prototype._getStandardMethodJson 353 request.locale = { _content: AjxEnv.DEFAULT_LOCALE }; 354 355 if (this.lastId) { // add lastSortVal and lastId for cursor-based paging 356 request.cursor = {id:this.lastId, sortVal:(this.lastSortVal || "")}; 357 } 358 if (this.sortBy) { 359 request.sortBy = this.sortBy; 360 } 361 if (this.conds && this.conds.length) { 362 request.searchFilter = {conds:{}}; 363 request.searchFilter.conds = ZmSearch.prototype._applyJsonCond(this.conds, request); 364 } 365 } else if (this.isAutocompleteSearch) { 366 jsonObj = {AutoCompleteRequest:{_jsns:"urn:zimbraMail"}}; 367 request = jsonObj.AutoCompleteRequest; 368 if (this.limit) { 369 request.limit = this.limit; 370 } 371 request.name = {_content:this.query}; 372 if (params.autocompleteType) { 373 request.t = params.autocompleteType; 374 } 375 } else if (this.isGalAutocompleteSearch) { 376 jsonObj = {AutoCompleteGalRequest:{_jsns:"urn:zimbraAccount"}}; 377 request = jsonObj.AutoCompleteGalRequest; 378 request.limit = this._getLimit(); 379 request.name = this.query; 380 if (this.galType) { 381 request.type = this.galType; 382 } 383 } else if (this.isCalResSearch) { 384 jsonObj = {SearchCalendarResourcesRequest:{_jsns:"urn:zimbraAccount"}}; 385 request = jsonObj.SearchCalendarResourcesRequest; 386 if (this.attrs) { 387 var attrs = [].concat(this.attrs); 388 request.attrs = attrs.join(","); 389 } 390 request.offset = this.offset = (this.offset || 0); 391 request.limit = this._getLimit(); 392 if (this.conds && this.conds.length) { 393 request.searchFilter = {conds:{}}; 394 request.searchFilter.conds = ZmSearch.prototype._applyJsonCond(this.conds, request); 395 } 396 } else { 397 if (this.soapInfo) { 398 soapDoc = AjxSoapDoc.create(this.soapInfo.method, this.soapInfo.namespace); 399 // Pass along any extra soap data. (Voice searches use this to pass user identification.) 400 for (var nodeName in this.soapInfo.additional) { 401 var node = soapDoc.set(nodeName); 402 var attrs = this.soapInfo.additional[nodeName]; 403 for (var attr in attrs) { 404 node.setAttribute(attr, attrs[attr]); 405 } 406 } 407 } else { 408 jsonObj = {SearchRequest:{_jsns:"urn:zimbraMail"}}; 409 request = jsonObj.SearchRequest; 410 } 411 this._getStandardMethodJson(request); 412 if (this.types) { 413 var a = this.types.getArray(); 414 if (a.length) { 415 var typeStr = []; 416 for (var i = 0; i < a.length; i++) { 417 typeStr.push(ZmSearch.TYPE[a[i]]); 418 } 419 request.types = typeStr.join(","); 420 421 if (this.types.contains(ZmItem.MSG) || this.types.contains(ZmItem.CONV)) { 422 // special handling for showing participants ("To" instead of "From") 423 var folder = this.folderId && appCtxt.getById(this.folderId); 424 request.recip = (folder && folder.isOutbound()) ? "2" : "0"; 425 } 426 427 if (this.types.contains(ZmItem.CONV)) { 428 // get ID/folder for every msg in each conv result 429 request.fullConversation = 1; 430 } 431 432 // if we're prefetching the first hit message, also mark it as read 433 if (this.fetch) { 434 request.fetch = ( this.fetch == "all" ) ? "all" : 1; 435 // and set the html flag if we want the html version 436 if (this.getHtml) { 437 request.html = 1; 438 } 439 } 440 441 if (this.markRead) { 442 request.read = 1; 443 } 444 445 if (this.headers) { 446 for (var hdr in this.headers) { 447 if (!request.header) { request.header = []; } 448 request.header.push({n: this.headers[hdr]}); 449 } 450 } 451 452 if (a.length == 1 && a[0] == ZmItem.TASK && this.allowableTaskStatus) { 453 request.allowableTaskStatus = this.allowableTaskStatus; 454 } 455 } 456 } 457 if (this.inDumpster) { 458 request.inDumpster = 1; 459 } 460 } 461 } 462 463 if (request) { 464 request.needExp = 1; 465 } 466 467 468 var respCallback = this._handleResponseExecute.bind(this, params.callback); 469 470 if (params.batchCmd) { 471 params.batchCmd.addRequestParams(soapDoc, respCallback); 472 } else { 473 var searchParams = { 474 jsonObj:jsonObj, 475 soapDoc:soapDoc, 476 asyncMode:true, 477 callback:respCallback, 478 errorCallback:params.errorCallback, 479 offlineCallback:params.offlineCallback, 480 timeout:params.timeout, 481 offlineCache:params.offlineCache, 482 noBusyOverlay:params.noBusyOverlay, 483 response:this.response, 484 accountName:this.accountName, 485 offlineRequest:params.offlineRequest 486 }; 487 return appCtxt.getAppController().sendRequest(searchParams); 488 } 489 }; 490 491 ZmSearch.prototype._applySoapCond = 492 function(inConds, soapDoc, condsEl, or) { 493 if (or || this.join == ZmSearch.JOIN_OR) { 494 condsEl.setAttribute("or", 1); 495 } 496 for (var i = 0; i < inConds.length; i++) { 497 var c = inConds[i]; 498 if (AjxUtil.isArray(c)) { 499 var subCondsEl = soapDoc.set("conds", null, condsEl); 500 this._applySoapCond(c, soapDoc, subCondsEl, true); 501 } else if (c.attr=="fullName" && c.op=="has") { 502 var nameEl = soapDoc.set("name", c.value); 503 } else { 504 var condEl = soapDoc.set("cond", null, condsEl); 505 condEl.setAttribute("attr", c.attr); 506 condEl.setAttribute("op", c.op); 507 condEl.setAttribute("value", c.value); 508 } 509 } 510 }; 511 512 ZmSearch.prototype._applyJsonCond = 513 function(inConds, request, or) { 514 var outConds = {}; 515 if (or || this.join == ZmSearch.JOIN_OR) { 516 outConds.or = 1; 517 } 518 519 for (var i = 0; i < inConds.length; i++) { 520 var c = inConds[i]; 521 if (AjxUtil.isArray(c)) { 522 if (!outConds.conds) 523 outConds.conds = []; 524 outConds.conds.push(this._applyJsonCond(c, request, true)); 525 } else if (c.attr=="fullName" && c.op=="has") { 526 request.name = {_content: c.value}; 527 } else { 528 if (!outConds.cond) 529 outConds.cond = []; 530 outConds.cond.push({attr:c.attr, op:c.op, value:c.value}); 531 } 532 } 533 return outConds; 534 }; 535 536 /** 537 * Converts the response into a {ZmSearchResult} and passes it along. 538 * 539 * @private 540 */ 541 ZmSearch.prototype._handleResponseExecute = 542 function(callback, result) { 543 544 if (result) { 545 var response = result.getResponse(); 546 547 if (this.isGalSearch) { response = response.SearchGalResponse; } 548 else if (this.isCalResSearch) { response = response.SearchCalendarResourcesResponse; } 549 else if (this.isAutocompleteSearch) { response = response.AutoCompleteResponse; } 550 else if (this.isGalAutocompleteSearch) { response = response.AutoCompleteGalResponse; } 551 else if (this.soapInfo) { response = response[this.soapInfo.response]; } 552 else { response = response.SearchResponse; } 553 } 554 else { 555 response = { _jsns: "urn:zimbraMail", more: false }; 556 } 557 var searchResult = new ZmSearchResult(this); 558 searchResult.set(response); 559 result = result || new ZmCsfeResult(); 560 result.set(searchResult); 561 562 if (callback) { 563 callback.run(result); 564 } 565 }; 566 567 /** 568 * Fetches a conversation from the server. 569 * 570 * @param {Hash} params a hash of parameters: 571 * @param {String} params.cid the conv ID 572 * @param {AjxCallback} params.callback the callback to run with result 573 * @param {String} params.fetch which msg bodies to load (see soap.txt) 574 * @param {Boolean} params.markRead if <code>true</code>, mark msg read 575 * @param {Boolean} params.noTruncate if <code>true</code>, do not limit size of msg 576 * @param {boolean} params.needExp if not <code>false</code>, have server check if addresses are DLs 577 */ 578 ZmSearch.prototype.getConv = 579 function(params) { 580 if ((!this.query && !this.queryHint) || !params.cid) { return; } 581 582 var jsonObj = {SearchConvRequest:{_jsns:"urn:zimbraMail"}}; 583 var request = jsonObj.SearchConvRequest; 584 this._getStandardMethodJson(request); 585 request.cid = params.cid; 586 if (params.fetch) { 587 request.fetch = params.fetch; 588 if (params.markRead) { 589 request.read = 1; // mark that msg read 590 } 591 if (this.getHtml) { 592 request.html = 1; // get it as HTML 593 } 594 if (params.needExp !== false) { 595 request.needExp = 1; 596 } 597 } 598 599 if (!params.noTruncate) { 600 request.max = appCtxt.get(ZmSetting.MAX_MESSAGE_SIZE); 601 } 602 603 //get both TO and From 604 request.recip = "2"; 605 606 var searchParams = { 607 jsonObj: jsonObj, 608 asyncMode: true, 609 callback: this._handleResponseGetConv.bind(this, params.callback), 610 accountName: this.accountName 611 }; 612 appCtxt.getAppController().sendRequest(searchParams); 613 }; 614 615 /** 616 * @private 617 */ 618 ZmSearch.prototype._handleResponseGetConv = 619 function(callback, result) { 620 var response = result.getResponse().SearchConvResponse; 621 var searchResult = new ZmSearchResult(this); 622 searchResult.set(response, null, true); 623 result.set(searchResult); 624 625 if (callback) { 626 callback.run(result); 627 } 628 }; 629 630 /** 631 * Clears cursor-related fields from this search so that it will not create a cursor. 632 */ 633 ZmSearch.prototype.clearCursor = 634 function() { 635 this.lastId = this.lastSortVal = this.endSortVal = null; 636 }; 637 638 /** 639 * Gets a title that summarizes this search. 640 * 641 * @return {String} the title 642 */ 643 ZmSearch.prototype.getTitle = 644 function() { 645 var where; 646 var pq = this.parsedQuery; 647 // if this is a saved search, show its name, otherwise show folder or tag name if it's the only term 648 var orgId = this.searchId || ((pq && (pq.getNumTokens() == 1)) ? this.folderId || this.tagId : null); 649 if (orgId) { 650 var org = appCtxt.getById(ZmOrganizer.getSystemId(orgId)); 651 if (org) { 652 where = org.getName(true, ZmOrganizer.MAX_DISPLAY_NAME_LENGTH, true); 653 } 654 } 655 return where ? ([ZmMsg.zimbraTitle, where].join(": ")) : ([ZmMsg.zimbraTitle, ZmMsg.searchResults].join(": ")); 656 }; 657 658 /** 659 * Checks if this search is multi-account. 660 * 661 * @return {Boolean} <code>true</code> if multi-account 662 */ 663 ZmSearch.prototype.isMultiAccount = 664 function() { 665 if (!this._isMultiAccount) { 666 this._isMultiAccount = (this.queryHint && this.queryHint.length > 0 && 667 (this.queryHint.split("inid:").length > 1 || 668 this.queryHint.split("underid:").length > 1)); 669 } 670 return this._isMultiAccount; 671 }; 672 673 /** 674 * @private 675 */ 676 ZmSearch.prototype._getStandardMethod = 677 function(soapDoc) { 678 679 var method = soapDoc.getMethod(); 680 681 if (this.sortBy) { 682 method.setAttribute("sortBy", this.sortBy); 683 } 684 685 if (this.types.contains(ZmItem.MSG) || this.types.contains(ZmItem.CONV)) { 686 ZmMailMsg.addRequestHeaders(soapDoc); 687 } 688 689 // bug 5771: add timezone and locale info 690 ZmTimezone.set(soapDoc, AjxTimezone.DEFAULT, null); 691 soapDoc.set("locale", appCtxt.get(ZmSetting.LOCALE_NAME), null); 692 693 if (this.lastId != null && this.lastSortVal) { 694 // cursor is used for paginated searches 695 var cursor = soapDoc.set("cursor"); 696 cursor.setAttribute("id", this.lastId); 697 cursor.setAttribute("sortVal", this.lastSortVal); 698 if (this.endSortVal) { 699 cursor.setAttribute("endSortVal", this.endSortVal); 700 } 701 } 702 703 this.offset = this.offset || 0; 704 method.setAttribute("offset", this.offset); 705 706 // always set limit 707 method.setAttribute("limit", this._getLimit()); 708 709 var query = this._getQuery(); 710 711 soapDoc.set("query", query); 712 713 // set search field if provided 714 if (this.field) { 715 method.setAttribute("field", this.field); 716 } 717 718 return method; 719 }; 720 721 /** 722 * @private 723 */ 724 ZmSearch.prototype._getStandardMethodJson = 725 function(req) { 726 727 if (this.sortBy) { 728 req.sortBy = this.sortBy; 729 } 730 731 if (this.types.contains(ZmItem.MSG) || this.types.contains(ZmItem.CONV)) { 732 ZmMailMsg.addRequestHeaders(req); 733 } 734 735 // bug 5771: add timezone and locale info 736 ZmTimezone.set(req, AjxTimezone.DEFAULT); 737 // bug 15878: We can't use appCtxt.get(ZmSetting.LOCALE) because that 738 // will return the server's default locale if it is not set 739 // set for the user or their COS. But AjxEnv.DEFAULT_LOCALE 740 // is set to the browser's locale setting in the case when 741 // the user's (or their COS) locale is not set. 742 req.locale = { _content: AjxEnv.DEFAULT_LOCALE }; 743 744 if (this.lastId != null && this.lastSortVal) { 745 // cursor is used for paginated searches 746 req.cursor = {id:this.lastId, sortVal:this.lastSortVal}; 747 if (this.endSortVal) { 748 req.cursor.endSortVal = this.endSortVal; 749 } 750 } 751 752 req.offset = this.offset = this.offset || 0; 753 754 // always set limit 755 req.limit = this._getLimit(); 756 757 if (this.idsOnly) { 758 req.resultMode = "IDS"; 759 } 760 761 req.query = this._getQuery(); 762 763 // set search field if provided 764 if (this.field) { 765 req.field = this.field; 766 } 767 }; 768 769 /** 770 * @private 771 */ 772 ZmSearch.prototype._getQuery = 773 function() { 774 // and of course, always set the query and append the query hint if applicable 775 // only use query hint if this is not a "simple" search 776 if (this.queryHint) { 777 var query = this.query ? ["(", this.query, ") "].join("") : ""; 778 return [query, "(", this.queryHint, ")"].join(""); 779 } 780 return this.query; 781 }; 782 783 /** 784 * @private 785 */ 786 ZmSearch.prototype._getLimit = 787 function() { 788 789 if (this.limit) { return this.limit; } 790 791 var limit; 792 if (this.isGalAutocompleteSearch) { 793 limit = appCtxt.get(ZmSetting.AUTOCOMPLETE_LIMIT); 794 } else { 795 var type = this.types && this.types.get(0); 796 var app = appCtxt.getApp(ZmItem.APP[type]) || appCtxt.getCurrentApp(); 797 if (app && app.getLimit) { 798 limit = app.getLimit(this.offset); 799 } else { 800 limit = appCtxt.get(ZmSetting.PAGE_SIZE) || ZmSearch.DEFAULT_LIMIT; 801 } 802 } 803 804 this.limit = limit; 805 return limit; 806 }; 807 808 /** 809 * Tests the given item against a matching function generated from the query. 810 * 811 * @param {ZmItem} item an item 812 * @return true if the item matches, false if it doesn't, and null if a matching function could not be generated 813 */ 814 ZmSearch.prototype.matches = 815 function(item) { 816 817 if (!this.parsedQuery) { 818 return null; 819 } 820 821 // if search is constrained to a folder, we can return false if item is not in that folder 822 if (this.folderId && !this.parsedQuery.hasOrTerm) { 823 if (item.type === ZmItem.CONV) { 824 if (item.folders && !item.folders[this.folderId]) { 825 return false; 826 } 827 } 828 else if (item.folderId && item.folderId !== this.folderId) { 829 return false; 830 } 831 } 832 833 var matchFunc = this.parsedQuery.getMatchFunction(); 834 return matchFunc ? matchFunc(item) : null; 835 }; 836 837 /** 838 * Returns true if the query has a folder-related term with the given value. 839 * 840 * @param {string} path a folder path (optional) 841 */ 842 ZmSearch.prototype.hasFolderTerm = 843 function(path) { 844 return this.parsedQuery && this.parsedQuery.hasTerm(["in", "under"], path); 845 }; 846 847 /** 848 * Replaces the old folder path with the new folder path in the query string, if found. 849 * 850 * @param {string} oldPath the old folder path 851 * @param {string} newPath the new folder path 852 * 853 * @return {boolean} true if replacement was performed 854 */ 855 ZmSearch.prototype.replaceFolderTerm = 856 function(oldPath, newPath) { 857 if (!this.parsedQuery) { 858 return this.query; 859 } 860 var newQuery = this.parsedQuery.replaceTerm(["in", "under"], oldPath, newPath); 861 if (newQuery) { 862 this.query = newQuery; 863 } 864 return Boolean(newQuery); 865 }; 866 867 /** 868 * Returns true if the query has a tag term with the given value. 869 * 870 * @param {string} tagName a tag name (optional) 871 */ 872 ZmSearch.prototype.hasTagTerm = 873 function(tagName) { 874 return this.parsedQuery && this.parsedQuery.hasTerm("tag", tagName); 875 }; 876 877 /** 878 * Replaces the old tag name with the new tag name in the query string, if found. 879 * 880 * @param {string} oldName the old tag name 881 * @param {string} newName the new tag name 882 * 883 * @return {boolean} true if replacement was performed 884 */ 885 ZmSearch.prototype.replaceTagTerm = 886 function(oldName, newName) { 887 if (!this.parsedQuery) { 888 return this.query; 889 } 890 var newQuery = this.parsedQuery.replaceTerm("tag", oldName, newName); 891 if (newQuery) { 892 this.query = newQuery; 893 } 894 return Boolean(newQuery); 895 }; 896 897 /** 898 * Returns true if the query has a term related to unread status. 899 */ 900 ZmSearch.prototype.hasUnreadTerm = 901 function() { 902 return (this.parsedQuery && (this.parsedQuery.hasTerm("is", "read") || 903 this.parsedQuery.hasTerm("is", "unread"))); 904 }; 905 906 /** 907 * Returns true if the query has the term "is:anywhere". 908 */ 909 ZmSearch.prototype.isAnywhere = 910 function() { 911 return (this.parsedQuery && this.parsedQuery.hasTerm("is", "anywhere")); 912 }; 913 914 /** 915 * Returns true if the query has a "content" term. 916 */ 917 ZmSearch.prototype.hasContentTerm = 918 function() { 919 return (this.parsedQuery && this.parsedQuery.hasTerm("content")); 920 }; 921 922 /** 923 * Returns true if the query has just one term, and it's a folder or tag term. 924 */ 925 ZmSearch.prototype.isSimple = 926 function() { 927 var pq = this.parsedQuery; 928 if (pq && (pq.getNumTokens() == 1)) { 929 return pq.hasTerm(["in", "inid", "tag"]); 930 } 931 return false; 932 }; 933 934 ZmSearch.prototype.getTokens = 935 function() { 936 return this.parsedQuery && this.parsedQuery.getTokens(); 937 }; 938 939 ZmSearch.prototype._setProperties = 940 function() { 941 var props = this.parsedQuery && this.parsedQuery.getProperties(); 942 for (var key in props) { 943 this[key] = props[key]; 944 } 945 }; 946 947 948 949 950 951 /** 952 * This class is a parsed representation of a query string. It parses the string into tokens. 953 * A token is a paren, a conditional operator, or a search term (which has an operator and an 954 * argument). The query string is assumed to be valid. 955 * 956 * Compound terms such as "in:(inbox or sent)" will be exploded into multiple terms. 957 * 958 * @param {string} query a query string 959 * 960 * TODO: handle "field[lastName]" and "#lastName" 961 */ 962 ZmParsedQuery = function(query) { 963 964 this.hasOrTerm = false; 965 this._tokens = this._parse(AjxStringUtil.trim(query, true)); 966 967 // preconditions for flags 968 if (!ZmParsedQuery.IS_VALUE_PRECONDITION) { 969 ZmParsedQuery.IS_VALUE_PRECONDITION = {}; 970 ZmParsedQuery.IS_VALUE_PRECONDITION['flagged'] = ZmSetting.FLAGGING_ENABLED; 971 ZmParsedQuery.IS_VALUE_PRECONDITION['unflagged'] = ZmSetting.FLAGGING_ENABLED; 972 } 973 }; 974 975 ZmParsedQuery.prototype.isZmParsedQuery = true; 976 ZmParsedQuery.prototype.toString = function() { return "ZmParsedQuery"; }; 977 978 ZmParsedQuery.TERM = "TERM"; // search operator such as "in" 979 ZmParsedQuery.COND = "COND"; // AND OR NOT 980 ZmParsedQuery.GROUP = "GROUP"; // ( or ) 981 982 ZmParsedQuery.OP_CONTENT = "content"; 983 984 ZmParsedQuery.OP_LIST = [ 985 "content", "subject", "msgid", "envto", "envfrom", "contact", "to", "from", "cc", "tofrom", 986 "tocc", "fromcc", "tofromcc", "in", "under", "inid", "underid", "has", "filename", "type", 987 "attachment", "is", "date", "mdate", "day", "week", "month", "year", "after", "before", 988 "size", "bigger", "larger", "smaller", "tag", "priority", "message", "my", "modseq", "conv", 989 "conv-count", "conv-minm", "conv-maxm", "conv-start", "conv-end", "appt-start", "appt-end", "author", "title", "keywords", 990 "company", "metadata", "item", "sort" 991 ]; 992 ZmParsedQuery.IS_OP = AjxUtil.arrayAsHash(ZmParsedQuery.OP_LIST); 993 994 // valid arguments for the search term "is:" 995 ZmParsedQuery.IS_VALUES = [ "unread", "read", "flagged", "unflagged", 996 "draft", "sent", "received", "replied", "unreplied", "forwarded", "unforwarded", 997 "invite", 998 "solo", 999 "tome", "fromme", "ccme", "tofromme", "toccme", "fromccme", "tofromccme", 1000 "local", "remote", "anywhere" ]; 1001 1002 // ops that can appear more than once in a query 1003 ZmParsedQuery.MULTIPLE = {}; 1004 ZmParsedQuery.MULTIPLE["to"] = true; 1005 ZmParsedQuery.MULTIPLE["is"] = true; 1006 ZmParsedQuery.MULTIPLE["has"] = true; 1007 ZmParsedQuery.MULTIPLE["tag"] = true; 1008 ZmParsedQuery.MULTIPLE["appt-start"] = true; 1009 ZmParsedQuery.MULTIPLE["appt-end"] = true; 1010 ZmParsedQuery.MULTIPLE["type"] = true; 1011 1012 ZmParsedQuery.isMultiple = 1013 function(term) { 1014 return Boolean(term && ZmParsedQuery.MULTIPLE[term.op]); 1015 }; 1016 1017 // ops that are mutually exclusive 1018 ZmParsedQuery.EXCLUDE = {}; 1019 ZmParsedQuery.EXCLUDE["before"] = ["date"]; 1020 ZmParsedQuery.EXCLUDE["after"] = ["date"]; 1021 1022 // values that are mutually exclusive - list value implies full multi-way exclusivity 1023 ZmParsedQuery.EXCLUDE["is"] = {}; 1024 ZmParsedQuery.EXCLUDE["is"]["read"] = ["unread"]; 1025 ZmParsedQuery.EXCLUDE["is"]["flagged"] = ["unflagged"]; 1026 ZmParsedQuery.EXCLUDE["is"]["sent"] = ["received"]; 1027 ZmParsedQuery.EXCLUDE["is"]["replied"] = ["unreplied"]; 1028 ZmParsedQuery.EXCLUDE["is"]["forwarded"] = ["unforwarded"]; 1029 ZmParsedQuery.EXCLUDE["is"]["local"] = ["remote", "anywhere"]; 1030 ZmParsedQuery.EXCLUDE["is"]["tome"] = ["tofromme", "toccme", "tofromccme"]; 1031 ZmParsedQuery.EXCLUDE["is"]["fromme"] = ["tofromme", "fromccme", "tofromccme"]; 1032 ZmParsedQuery.EXCLUDE["is"]["ccme"] = ["toccme", "fromccme", "tofromccme"]; 1033 1034 ZmParsedQuery._createExcludeMap = 1035 function(excludes) { 1036 1037 var excludeMap = {}; 1038 for (var key in excludes) { 1039 var value = excludes[key]; 1040 if (AjxUtil.isArray1(value)) { 1041 value.push(key); 1042 ZmParsedQuery._permuteExcludeMap(excludeMap, value); 1043 } 1044 else { 1045 for (var key1 in value) { 1046 var value1 = excludes[key][key1]; 1047 value1.push(key1); 1048 ZmParsedQuery._permuteExcludeMap(excludeMap, AjxUtil.map(value1, 1049 function(val) { 1050 return new ZmSearchToken(key, val).toString(); 1051 })); 1052 } 1053 } 1054 } 1055 return excludeMap; 1056 }; 1057 1058 // makes each possible pair in the list exclusive 1059 ZmParsedQuery._permuteExcludeMap = 1060 function(excludeMap, list) { 1061 if (list.length < 2) { return; } 1062 for (var i = 0; i < list.length - 1; i++) { 1063 var a = list[i]; 1064 for (var j = i + 1; j < list.length; j++) { 1065 var b = list[j]; 1066 excludeMap[a] = excludeMap[a] || {}; 1067 excludeMap[b] = excludeMap[b] || {}; 1068 excludeMap[a][b] = true; 1069 excludeMap[b][a] = true; 1070 } 1071 } 1072 }; 1073 1074 /** 1075 * Returns true if the given search terms should not appear in the same query. 1076 * 1077 * @param {ZmSearchToken} termA search term 1078 * @param {ZmSearchToken} termB search term 1079 */ 1080 ZmParsedQuery.areExclusive = 1081 function(termA, termB) { 1082 if (!termA || !termB) { return false; } 1083 var map = ZmParsedQuery.EXCLUDE_MAP; 1084 if (!map) { 1085 map = ZmParsedQuery.EXCLUDE_MAP = ZmParsedQuery._createExcludeMap(ZmParsedQuery.EXCLUDE); 1086 } 1087 var opA = termA.op, opB = termB.op; 1088 var strA = termA.toString(), strB = termB.toString(); 1089 return Boolean((map[opA] && map[opA][opB]) || (map[opB] && map[opB][opA]) || 1090 (map[strA] && map[strA][strB]) || (map[strB] && map[strB][strA])); 1091 }; 1092 1093 // conditional ops 1094 ZmParsedQuery.COND_AND = "and" 1095 ZmParsedQuery.COND_OR = "or"; 1096 ZmParsedQuery.COND_NOT = "not"; 1097 ZmParsedQuery.GROUP_OPEN = "("; 1098 ZmParsedQuery.GROUP_CLOSE = ")"; 1099 1100 // JS version of conditional 1101 ZmParsedQuery.COND_OP = {}; 1102 ZmParsedQuery.COND_OP[ZmParsedQuery.COND_AND] = " && "; 1103 ZmParsedQuery.COND_OP[ZmParsedQuery.COND_OR] = " || "; 1104 ZmParsedQuery.COND_OP[ZmParsedQuery.COND_NOT] = " !"; 1105 1106 // word separators 1107 ZmParsedQuery.EOW_LIST = [" ", ":", ZmParsedQuery.GROUP_OPEN, ZmParsedQuery.GROUP_CLOSE]; 1108 ZmParsedQuery.IS_EOW = AjxUtil.arrayAsHash(ZmParsedQuery.EOW_LIST); 1109 1110 // map is:xxx to item properties 1111 ZmParsedQuery.FLAG = {}; 1112 ZmParsedQuery.FLAG["unread"] = "item.isUnread"; 1113 ZmParsedQuery.FLAG["read"] = "!item.isUnread"; 1114 ZmParsedQuery.FLAG["flagged"] = "item.isFlagged"; 1115 ZmParsedQuery.FLAG["unflagged"] = "!item.isFlagged"; 1116 ZmParsedQuery.FLAG["forwarded"] = "item.isForwarded"; 1117 ZmParsedQuery.FLAG["unforwarded"] = "!item.isForwarded"; 1118 ZmParsedQuery.FLAG["sent"] = "item.isSent"; 1119 ZmParsedQuery.FLAG["draft"] = "item.isDraft"; 1120 ZmParsedQuery.FLAG["replied"] = "item.isReplied"; 1121 ZmParsedQuery.FLAG["unreplied"] = "!item.isReplied"; 1122 1123 ZmParsedQuery.prototype._parse = 1124 function(query) { 1125 1126 function getQuotedStr(str, pos, q) { 1127 var q = q || str.charAt(pos); 1128 pos++; 1129 var done = false, ch, quoted = ""; 1130 while (pos < str.length && !done) { 1131 ch = str.charAt(pos); 1132 if (ch == q) { 1133 done = true; 1134 } else { 1135 quoted += ch; 1136 pos++; 1137 } 1138 } 1139 1140 return done ? {str:quoted, pos:pos + 1} : null; 1141 } 1142 1143 function skipSpace(str, pos) { 1144 while (pos < str.length && str.charAt(pos) == " ") { 1145 pos++; 1146 } 1147 return pos; 1148 } 1149 1150 function fail(reason, query) { 1151 DBG.println(AjxDebug.DBG1, "ZmParsedQuery failure: " + reason + "; query: [" + query + "]"); 1152 this.parseFailed = reason; 1153 return tokens; 1154 } 1155 1156 var len = query.length; 1157 var tokens = [], ch, lastCh, op, word = "", isEow = false, endOk = true, compound = 0, numParens = 0; 1158 var pos = skipSpace(query, 0); 1159 while (pos < len) { 1160 lastCh = (ch != " ") ? ch : lastCh; 1161 ch = query.charAt(pos); 1162 isEow = ZmParsedQuery.IS_EOW[ch]; 1163 1164 if (ch == ":") { 1165 if (ZmParsedQuery.IS_OP[word]) { 1166 op = word; 1167 } else { 1168 return fail("unrecognized op '" + word + "'", query); 1169 } 1170 word = ""; 1171 pos = skipSpace(query, pos + 1); 1172 continue; 1173 } 1174 1175 if (isEow) { 1176 var lcWord = word.toLowerCase(); 1177 var isCondOp = !!ZmParsedQuery.COND_OP[lcWord]; 1178 if (op && word && !(isCondOp && compound > 0)) { 1179 tokens.push(new ZmSearchToken(op, lcWord)); 1180 if (compound == 0) { 1181 op = ""; 1182 } 1183 word = ""; 1184 endOk = true; 1185 } else if (!op || (op && compound > 0)) { 1186 if (isCondOp) { 1187 tokens.push(new ZmSearchToken(lcWord)); 1188 endOk = false; 1189 if (lcWord == ZmParsedQuery.COND_OR) { 1190 this.hasOrTerm = true; 1191 } 1192 } else if (word) { 1193 tokens.push(new ZmSearchToken(ZmParsedQuery.OP_CONTENT, word)); 1194 } 1195 word = ""; 1196 } 1197 } 1198 1199 if (ch == '"') { 1200 var results = getQuotedStr(query, pos); 1201 if (results) { 1202 word = results.str; 1203 pos = results.pos; 1204 } else { 1205 return fail("improper use of quotes", query); 1206 } 1207 } else if (ch == ZmParsedQuery.GROUP_OPEN) { 1208 var done = false; 1209 if (compound > 0) { 1210 compound++; 1211 } 1212 else if (lastCh == ":") { 1213 compound = 1; 1214 // see if parens are being used as secondary quoting mechanism by looking for and/or 1215 var inside = query.substr(pos, query.indexOf(ZmParsedQuery.GROUP_CLOSE, pos + 1)); 1216 inside = inside && inside.toLowerCase(); 1217 if (inside && (inside.indexOf(" " + ZmParsedQuery.COND_OR + " ") == -1) && 1218 (inside.indexOf(" " + ZmParsedQuery.COND_AND + " ") == -1)) { 1219 var results = getQuotedStr(query, pos, ZmParsedQuery.GROUP_CLOSE); 1220 if (results) { 1221 word = results.str; 1222 pos = results.pos; 1223 compound = 0; 1224 } else { 1225 return fail("improper use of paren-based quoting", query); 1226 } 1227 done = true; 1228 } 1229 } 1230 if (!done) { 1231 tokens.push(new ZmSearchToken(ch)); 1232 numParens++; 1233 } 1234 pos = skipSpace(query, pos + 1); 1235 } else if (ch == ZmParsedQuery.GROUP_CLOSE) { 1236 if (compound > 0) { 1237 compound--; 1238 } 1239 if (compound == 0) { 1240 op = ""; 1241 } 1242 tokens.push(new ZmSearchToken(ch)); 1243 pos = skipSpace(query, pos + 1); 1244 } else if (ch == "-" && !word && !op) { 1245 tokens.push(new ZmSearchToken(ZmParsedQuery.COND_NOT)); 1246 pos = skipSpace(query, pos + 1); 1247 endOk = false; 1248 } else { 1249 if (ch != " ") { 1250 word += ch; 1251 } 1252 pos++; 1253 } 1254 } 1255 1256 // check for term at end 1257 if ((pos >= query.length) && op && word) { 1258 tokens.push(new ZmSearchToken(op, word)); 1259 endOk = true; 1260 } else if (!op && word) { 1261 tokens.push(new ZmSearchToken(word)); 1262 } 1263 1264 // remove unnecessary enclosing parens from when a single compound term is expanded, for example when 1265 // "subject:(foo bar)" is expanded into "(subject:foo subject:bar)" 1266 if (tokens.length >= 3 && numParens == 1 && tokens[0].op == ZmParsedQuery.GROUP_OPEN && 1267 tokens[tokens.length - 1].op == ZmParsedQuery.GROUP_CLOSE) { 1268 tokens.shift(); 1269 tokens.pop(); 1270 } 1271 1272 if (!endOk) { 1273 return fail("unexpected end of query", query); 1274 } 1275 1276 return tokens; 1277 }; 1278 1279 ZmParsedQuery.prototype.getTokens = 1280 function() { 1281 return this._tokens; 1282 }; 1283 1284 ZmParsedQuery.prototype.getNumTokens = 1285 function() { 1286 return this._tokens ? this._tokens.length : 0; 1287 }; 1288 1289 ZmParsedQuery.prototype.getProperties = 1290 function() { 1291 1292 var props = {}; 1293 for (var i = 0, len = this._tokens.length; i < len; i++) { 1294 var t = this._tokens[i]; 1295 if (t.type == ZmParsedQuery.TERM) { 1296 var prev = i > 0 ? this._tokens[i-1] : null; 1297 if (!((prev && prev.op == ZmParsedQuery.COND_NOT) || this.hasOrTerm)) { 1298 if ((t.op == "in" || t.op == "inid") ) { 1299 this.folderId = props.folderId = (t.op == "in") ? this._getFolderId(t.arg) : t.arg; 1300 } else if (t.op == "tag") { 1301 // TODO: make sure there's only one tag term? 1302 this.tagId = props.tagId = this._getTagId(t.arg, true); 1303 } 1304 } 1305 } 1306 } 1307 return props; 1308 }; 1309 1310 /** 1311 * Returns a function based on the parsed query. The function is passed an item (msg or conv) and returns 1312 * true if the item matches the search. 1313 * 1314 * @return {Function} the match function 1315 */ 1316 ZmParsedQuery.prototype.getMatchFunction = 1317 function() { 1318 1319 if (this._matchFunction) { 1320 return this._matchFunction; 1321 } 1322 if (this.parseFailed || this.hasTerm(ZmParsedQuery.OP_CONTENT)) { 1323 return null; 1324 } 1325 1326 var folderId, tagId; 1327 var func = ["return Boolean("]; 1328 for (var i = 0, len = this._tokens.length; i < len; i++) { 1329 var t = this._tokens[i]; 1330 if (t.type === ZmParsedQuery.TERM) { 1331 if (t.op === "in" || t.op === "inid") { 1332 folderId = (t.op === "in") ? this._getFolderId(t.arg) : t.arg; 1333 if (folderId) { 1334 func.push("((item.type === ZmItem.CONV) ? item.folders && item.folders['" + folderId +"'] : item.folderId === '" + folderId + "')"); 1335 } 1336 } 1337 else if (t.op === "tag") { 1338 tagId = this._getTagId(t.arg, true); 1339 if (tagId) { 1340 func.push("item.hasTag('" + t.arg + "')"); 1341 } 1342 } 1343 else if (t.op === "is") { 1344 var test = ZmParsedQuery.FLAG[t.arg]; 1345 if (test) { 1346 func.push(test); 1347 } 1348 } 1349 else if (t.op === 'has' && t.arg === 'attachment') { 1350 func.push("item.hasAttach"); 1351 } 1352 else { 1353 // search had a term we don't know how to match 1354 return null; 1355 } 1356 var next = this._tokens[i + 1]; 1357 if (next && (next.type == ZmParsedQuery.TERM || next == ZmParsedQuery.COND_OP[ZmParsedQuery.COND_NOT] || next == ZmParsedQuery.GROUP_CLOSE)) { 1358 func.push(ZmParsedQuery.COND_OP[ZmParsedQuery.COND_AND]); 1359 } 1360 } 1361 else if (t.type === ZmParsedQuery.COND) { 1362 func.push(ZmParsedQuery.COND_OP[t.op]); 1363 } 1364 else if (t.type === ZmParsedQuery.GROUP) { 1365 func.push(t.op); 1366 } 1367 } 1368 func.push(")"); 1369 1370 // the way multi-account searches are done, we set the queryHint *only* so 1371 // set the folderId if it exists for simple multi-account searches 1372 // TODO: multi-acct part seems wrong; search with many folders joined by OR would incorrectly set folderId to last folder 1373 var isMultiAccountSearch = (appCtxt.multiAccounts && this.isMultiAccount() && !this.query && this.queryHint); 1374 if (!this.hasOrTerm || isMultiAccountSearch) { 1375 this.folderId = folderId; 1376 this.tagId = tagId; 1377 } 1378 1379 try { 1380 this._matchFunction = new Function("item", func.join("")); 1381 } catch(ex) {} 1382 1383 return this._matchFunction; 1384 }; 1385 1386 /** 1387 * Returns a query string that should be logically equivalent to the original query. 1388 */ 1389 ZmParsedQuery.prototype.createQuery = 1390 function() { 1391 var terms = []; 1392 for (var i = 0, len = this._tokens.length; i < len; i++) { 1393 terms.push(this._tokens[i].toString()); 1394 } 1395 return terms.join(" "); 1396 }; 1397 1398 // Returns the fully-qualified ID for the given folder path. 1399 ZmParsedQuery.prototype._getFolderId = 1400 function(path) { 1401 // first check if it's a system folder (name in query string may not match actual name) 1402 var folderId = ZmFolder.QUERY_ID[path]; 1403 1404 var accountName = this.accountName; 1405 if (!accountName) { 1406 var active = appCtxt.getActiveAccount(); 1407 accountName = active ? active.name : appCtxt.accountList.mainAccount; 1408 } 1409 1410 // now check all folders by name 1411 if (!folderId) { 1412 var account = accountName && appCtxt.accountList.getAccountByName(accountName); 1413 var folders = appCtxt.getFolderTree(account); 1414 var folder = folders ? folders.getByPath(path, true) : null; 1415 if (folder) { 1416 folderId = folder.id; 1417 } 1418 } 1419 1420 if (accountName) { 1421 folderId = ZmOrganizer.getSystemId(folderId, appCtxt.accountList.getAccountByName(accountName)); 1422 } 1423 1424 return folderId; 1425 }; 1426 1427 // Returns the ID for the given tag name. 1428 ZmParsedQuery.prototype._getTagId = 1429 function(name, normalized) { 1430 var tagTree = appCtxt.getTagTree(); 1431 if (tagTree) { 1432 var tag = tagTree.getByName(name.toLowerCase()); 1433 if (tag) { 1434 return normalized ? tag.nId : tag.id; 1435 } 1436 } 1437 return null; 1438 }; 1439 1440 /** 1441 * Gets the given term with the given argument. Case-insensitive. Returns the first term found. 1442 * 1443 * @param {array} opList list of ops 1444 * @param {string} value argument value (optional) 1445 * 1446 * @return {object} a token object, or null 1447 */ 1448 ZmParsedQuery.prototype.getTerm = 1449 function(opList, value) { 1450 var opHash = AjxUtil.arrayAsHash(opList); 1451 var lcValue = value && value.toLowerCase(); 1452 for (var i = 0, len = this._tokens.length; i < len; i++) { 1453 var t = this._tokens[i]; 1454 var lcArg = t.arg && t.arg.toLowerCase(); 1455 if (t.type == ZmParsedQuery.TERM && opHash[t.op] && (!value || lcArg == lcValue)) { 1456 return t; 1457 } 1458 } 1459 return null; 1460 }; 1461 1462 /** 1463 * Returns true if the query contains the given term with the given argument. Case-insensitive. 1464 * 1465 * @param {array} opList list of ops 1466 * @param {string} value argument value (optional) 1467 * 1468 * @return {boolean} true if the query contains the given term with the given argument 1469 */ 1470 ZmParsedQuery.prototype.hasTerm = 1471 function(opList, value) { 1472 return Boolean(this.getTerm(opList, value)); 1473 }; 1474 1475 /** 1476 * Replaces the argument within the query for the given ops, if found. Case-insensitive. Replaces 1477 * only the first match. 1478 * 1479 * @param {array} opList list of ops 1480 * @param {string} oldValue the old argument 1481 * @param {string} newValue the new argument 1482 * 1483 * @return {string} a new query string (if the old argument was found and replaced), or the empty string 1484 */ 1485 ZmParsedQuery.prototype.replaceTerm = 1486 function(opList, oldValue, newValue) { 1487 var lcValue = oldValue && oldValue.toLowerCase(); 1488 var opHash = AjxUtil.arrayAsHash(opList); 1489 if (oldValue && newValue) { 1490 for (var i = 0, len = this._tokens.length; i < len; i++) { 1491 var t = this._tokens[i]; 1492 var lcArg = t.arg && t.arg.toLowerCase(); 1493 if (t.type == ZmParsedQuery.TERM && opHash[t.op] && (lcArg == lcValue)) { 1494 t.arg = newValue; 1495 return this.createQuery(); 1496 } 1497 } 1498 } 1499 return ""; 1500 }; 1501 1502 /** 1503 * This class represents one unit of a search query. That may be a search term ("is:unread"), 1504 * and conditional operator (AND, OR, NOT), or a grouping operator (left or right paren). 1505 * 1506 * @param {string} op operator 1507 * @param {string} arg argument part of search term 1508 */ 1509 ZmSearchToken = function(op, arg) { 1510 1511 if (op && arguments.length == 1) { 1512 var parts = op.split(":"); 1513 op = parts[0]; 1514 arg = parts[1]; 1515 } 1516 1517 this.op = op; 1518 this.arg = arg; 1519 if (ZmParsedQuery.IS_OP[op] && arg) { 1520 this.type = ZmParsedQuery.TERM; 1521 } 1522 else if (op && ZmParsedQuery.COND_OP[op.toLowerCase()]) { 1523 this.type = ZmParsedQuery.COND; 1524 this.op = op.toLowerCase(); 1525 } 1526 else if (op == ZmParsedQuery.GROUP_OPEN || op == ZmParsedQuery.GROUP_CLOSE) { 1527 this.type = ZmParsedQuery.GROUP; 1528 } else if (op) { 1529 this.type = ZmParsedQuery.TERM; 1530 this.op = ZmParsedQuery.OP_CONTENT; 1531 this.arg = op; 1532 } 1533 }; 1534 1535 ZmSearchToken.prototype.isZmSearchToken = true; 1536 1537 /** 1538 * Returns the string version of this token. 1539 * 1540 * @param {boolean} force if true, return "and" instead of an empty string ("and" is implied) 1541 */ 1542 ZmSearchToken.prototype.toString = 1543 function(force) { 1544 if (this.type == ZmParsedQuery.TERM) { 1545 var arg = this.arg; 1546 if (this.op == ZmParsedQuery.OP_CONTENT) { 1547 return /\W/.test(arg) ? '"' + arg.replace(/"/g, '\\"') + '"' : arg; 1548 } 1549 else { 1550 // quote arg if it has any spaces and is not already quoted 1551 arg = (arg && (arg.indexOf('"') !== 0) && arg.indexOf(" ") != -1) ? '"' + arg + '"' : arg; 1552 return [this.op, arg].join(":"); 1553 } 1554 } 1555 else { 1556 return (!force && this.op == ZmParsedQuery.COND_AND) ? "" : this.op; 1557 } 1558 }; 1559