summaryrefslogtreecommitdiffstats
path: root/mailnews/addrbook/src/nsAbLDAPAutoCompleteSearch.js
diff options
context:
space:
mode:
Diffstat (limited to 'mailnews/addrbook/src/nsAbLDAPAutoCompleteSearch.js')
-rw-r--r--mailnews/addrbook/src/nsAbLDAPAutoCompleteSearch.js325
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]);