diff options
Diffstat (limited to 'mailnews/addrbook/src/nsAbLDAPAutoCompleteSearch.js')
-rw-r--r-- | mailnews/addrbook/src/nsAbLDAPAutoCompleteSearch.js | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/mailnews/addrbook/src/nsAbLDAPAutoCompleteSearch.js b/mailnews/addrbook/src/nsAbLDAPAutoCompleteSearch.js new file mode 100644 index 000000000..1c62d7e97 --- /dev/null +++ b/mailnews/addrbook/src/nsAbLDAPAutoCompleteSearch.js @@ -0,0 +1,325 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +Components.utils.import("resource:///modules/mailServices.js"); +Components.utils.import("resource://gre/modules/Services.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +var ACR = Components.interfaces.nsIAutoCompleteResult; +var nsIAbAutoCompleteResult = Components.interfaces.nsIAbAutoCompleteResult; +var nsIAbDirectoryQueryResultListener = + Components.interfaces.nsIAbDirectoryQueryResultListener; + +// nsAbLDAPAutoCompleteResult +// Derived from nsIAbAutoCompleteResult, provides a LDAP specific result +// implementation. + +function nsAbLDAPAutoCompleteResult(aSearchString) { + // Can't create this in the prototype as we'd get the same array for + // all instances + this._searchResults = []; + this.searchString = aSearchString; +} + +nsAbLDAPAutoCompleteResult.prototype = { + _searchResults: null, + _commentColumn: "", + + // nsIAutoCompleteResult + + searchString: null, + searchResult: ACR.RESULT_NOMATCH, + defaultIndex: -1, + errorDescription: null, + + get matchCount() { + return this._searchResults.length; + }, + + getLabelAt: function getLabelAt(aIndex) { + return this.getValueAt(aIndex); + }, + + getValueAt: function getValueAt(aIndex) { + return this._searchResults[aIndex].value; + }, + + getCommentAt: function getCommentAt(aIndex) { + return this._commentColumn; + }, + + getStyleAt: function getStyleAt(aIndex) { + return this.searchResult == ACR.RESULT_FAILURE ? "remote-err" : + "remote-abook"; + }, + + getImageAt: function getImageAt(aIndex) { + return ""; + }, + + getFinalCompleteValueAt: function(aIndex) { + return this.getValueAt(aIndex); + }, + + removeValueAt: function removeValueAt(aRowIndex, aRemoveFromDB) { + }, + + // nsIAbAutoCompleteResult + + getCardAt: function getCardAt(aIndex) { + return this._searchResults[aIndex].card; + }, + + // nsISupports + + QueryInterface: XPCOMUtils.generateQI([ACR, nsIAbAutoCompleteResult]) +} + +function nsAbLDAPAutoCompleteSearch() { + Services.obs.addObserver(this, "quit-application", false); + this._timer = Components.classes["@mozilla.org/timer;1"] + .createInstance(Components.interfaces.nsITimer); +} + +nsAbLDAPAutoCompleteSearch.prototype = { + // For component registration + classID: Components.ID("227e6482-fe9f-441f-9b7d-7b60375e7449"), + + // A short-lived LDAP directory cache. + // To avoid recreating components as the user completes, we maintain the most + // recently used address book, nsAbLDAPDirectoryQuery and search context. + // However the cache is discarded if it has not been used for a minute. + // This is done to avoid problems with LDAP sessions timing out and hanging. + _query: null, + _book: null, + _attributes: null, + _context: -1, + _timer: null, + + // The current search result. + _result: null, + // The listener to pass back results to. + _listener: null, + + _parser: MailServices.headerParser, + + applicableHeaders: new Set(["addr_to", "addr_cc", "addr_bcc", "addr_reply"]), + + // Private methods + + _checkDuplicate: function _checkDuplicate(card, emailAddress) { + var lcEmailAddress = emailAddress.toLocaleLowerCase(); + + return this._result._searchResults.some(function(result) { + return result.value.toLocaleLowerCase() == lcEmailAddress; + }); + }, + + _addToResult: function(card) { + let mbox = this._parser.makeMailboxObject(card.displayName, + card.isMailList ? card.getProperty("Notes", "") || card.displayName : + card.primaryEmail); + if (!mbox.email) + return; + + let emailAddress = mbox.toString(); + + // If it is a duplicate, then just return and don't add it. The + // _checkDuplicate function deals with it all for us. + if (this._checkDuplicate(card, emailAddress)) + return; + + // Find out where to insert the card. + var insertPosition = 0; + + // Next sort on full address + while (insertPosition < this._result._searchResults.length && + emailAddress > this._result._searchResults[insertPosition].value) + ++insertPosition; + + this._result._searchResults.splice(insertPosition, 0, { + value: emailAddress, + card: card, + }); + }, + + // nsIObserver + + observe: function observer(subject, topic, data) { + if (topic == "quit-application") { + Services.obs.removeObserver(this, "quit-application"); + } else if (topic != "timer-callback") { + return; + } + + // Force the individual query items to null, so that the memory + // gets collected straight away. + this.stopSearch(); + this._book = null; + this._context = -1; + this._query = null; + this._attributes = null; + }, + + // nsIAutoCompleteSearch + + startSearch: function startSearch(aSearchString, aParam, + aPreviousResult, aListener) { + let params = JSON.parse(aParam) || {}; + let applicable = !("type" in params) || this.applicableHeaders.has(params.type); + + this._result = new nsAbLDAPAutoCompleteResult(aSearchString); + aSearchString = aSearchString.toLocaleLowerCase(); + + // If the search string isn't value, or contains a comma, or the user + // hasn't enabled autocomplete, then just return no matches / or the + // result ignored. + // The comma check is so that we don't autocomplete against the user + // entering multiple addresses. + if (!applicable || !aSearchString || aSearchString.includes(",")) { + this._result.searchResult = ACR.RESULT_IGNORED; + aListener.onSearchResult(this, this._result); + return; + } + + // The rules here: If the current identity has a directoryServer set, then + // use that, otherwise, try the global preference instead. + var acDirURI = null; + var identity; + + if ("idKey" in params) { + try { + identity = MailServices.accounts.getIdentity(params.idKey); + } + catch(ex) { + Components.utils.reportError("Couldn't get specified identity, " + + "falling back to global settings"); + } + } + + // Does the current identity override the global preference? + if (identity && identity.overrideGlobalPref) + acDirURI = identity.directoryServer; + else { + // Try the global one + if (Services.prefs.getBoolPref("ldap_2.autoComplete.useDirectory")) + acDirURI = Services.prefs.getCharPref("ldap_2.autoComplete.directoryServer"); + } + + if (!acDirURI) { + // No directory to search, send a no match and return. + aListener.onSearchResult(this, this._result); + return; + } + + this.stopSearch(); + + // If we don't already have a cached query for this URI, build a new one. + acDirURI = "moz-abldapdirectory://" + acDirURI; + if (!this._book || this._book.URI != acDirURI) { + this._query = + Components.classes["@mozilla.org/addressbook/ldap-directory-query;1"] + .createInstance(Components.interfaces.nsIAbDirectoryQuery); + this._book = MailServices.ab.getDirectory(acDirURI) + .QueryInterface(Components.interfaces.nsIAbLDAPDirectory); + + // Create a minimal map just for the display name and primary email. + this._attributes = + Components.classes["@mozilla.org/addressbook/ldap-attribute-map;1"] + .createInstance(Components.interfaces.nsIAbLDAPAttributeMap); + this._attributes.setAttributeList("DisplayName", + this._book.attributeMap.getAttributeList("DisplayName", {}), true); + this._attributes.setAttributeList("PrimaryEmail", + this._book.attributeMap.getAttributeList("PrimaryEmail", {}), true); + } + + this._result._commentColumn = this._book.dirName; + this._listener = aListener; + this._timer.init(this, 60000, Components.interfaces.nsITimer.TYPE_ONE_SHOT); + + var args = + Components.classes["@mozilla.org/addressbook/directory/query-arguments;1"] + .createInstance(Components.interfaces.nsIAbDirectoryQueryArguments); + + var filterTemplate = this._book.getStringValue("autoComplete.filterTemplate", ""); + + // Use default value when preference is not set or it contains empty string + if (!filterTemplate) + filterTemplate = "(|(cn=%v1*%v2-*)(mail=%v1*%v2-*)(sn=%v1*%v2-*))"; + + // Create filter from filter template and search string + var ldapSvc = Components.classes["@mozilla.org/network/ldap-service;1"] + .getService(Components.interfaces.nsILDAPService); + var filter = ldapSvc.createFilter(1024, filterTemplate, "", "", "", aSearchString); + if (!filter) + throw new Error("Filter string is empty, check if filterTemplate variable is valid in prefs.js."); + args.typeSpecificArg = this._attributes; + args.querySubDirectories = true; + args.filter = filter; + + // Start the actual search + this._context = + this._query.doQuery(this._book, args, this, this._book.maxHits, 0); + }, + + stopSearch: function stopSearch() { + if (this._listener) { + this._query.stopQuery(this._context); + this._listener = null; + } + }, + + // nsIAbDirSearchListener + + onSearchFinished: function onSearchFinished(aResult, aErrorMsg) { + if (!this._listener) + return; + + if (aResult == nsIAbDirectoryQueryResultListener.queryResultComplete) { + if (this._result.matchCount) { + this._result.searchResult = ACR.RESULT_SUCCESS; + this._result.defaultIndex = 0; + } + else + this._result.searchResult = ACR.RESULT_NOMATCH; + } + else if (aResult == nsIAbDirectoryQueryResultListener.queryResultError) { + this._result.searchResult = ACR.RESULT_FAILURE; + this._result.defaultIndex = 0; + } + // const long queryResultStopped = 2; + // const long queryResultError = 3; + this._listener.onSearchResult(this, this._result); + this._listener = null; + }, + + onSearchFoundCard: function onSearchFoundCard(aCard) { + if (!this._listener) + return; + + this._addToResult(aCard); + + /* XXX autocomplete doesn't expect you to rearrange while searching + if (this._result.matchCount) + this._result.searchResult = ACR.RESULT_SUCCESS_ONGOING; + else + this._result.searchResult = ACR.RESULT_NOMATCH_ONGOING; + + this._listener.onSearchResult(this, this._result); + */ + }, + + // nsISupports + + QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver, + Components.interfaces + .nsIAutoCompleteSearch, + Components.interfaces + .nsIAbDirSearchListener]) +}; + +// Module + +var NSGetFactory = XPCOMUtils.generateNSGetFactory([nsAbLDAPAutoCompleteSearch]); |