summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/addrbook/src/AbLDAPAutoCompleteSearch.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/addrbook/src/AbLDAPAutoCompleteSearch.jsm')
-rw-r--r--comm/mailnews/addrbook/src/AbLDAPAutoCompleteSearch.jsm364
1 files changed, 364 insertions, 0 deletions
diff --git a/comm/mailnews/addrbook/src/AbLDAPAutoCompleteSearch.jsm b/comm/mailnews/addrbook/src/AbLDAPAutoCompleteSearch.jsm
new file mode 100644
index 0000000000..8df1c0f71e
--- /dev/null
+++ b/comm/mailnews/addrbook/src/AbLDAPAutoCompleteSearch.jsm
@@ -0,0 +1,364 @@
+/* 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/. */
+
+var EXPORTED_SYMBOLS = ["AbLDAPAutoCompleteSearch"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var ACR = Ci.nsIAutoCompleteResult;
+
+// 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(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ getValueAt(aIndex) {
+ return this._searchResults[aIndex].value;
+ },
+
+ getCommentAt(aIndex) {
+ return this._commentColumn;
+ },
+
+ getStyleAt(aIndex) {
+ return this.searchResult == ACR.RESULT_FAILURE
+ ? "remote-err"
+ : "remote-abook";
+ },
+
+ getImageAt(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ removeValueAt(aRowIndex, aRemoveFromDB) {},
+
+ // nsIAbAutoCompleteResult
+
+ getCardAt(aIndex) {
+ return this._searchResults[aIndex].card;
+ },
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIAutoCompleteResult",
+ "nsIAbAutoCompleteResult",
+ ]),
+};
+
+function AbLDAPAutoCompleteSearch() {
+ Services.obs.addObserver(this, "quit-application");
+ this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+}
+
+AbLDAPAutoCompleteSearch.prototype = {
+ // 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(card, emailAddress) {
+ var lcEmailAddress = emailAddress.toLocaleLowerCase();
+
+ return this._result._searchResults.some(function (result) {
+ return result.value.toLocaleLowerCase() == lcEmailAddress;
+ });
+ },
+
+ _addToResult(card, address) {
+ let mbox = this._parser.makeMailboxObject(
+ card.displayName,
+ card.isMailList
+ ? card.getProperty("Notes", "") || card.displayName
+ : address
+ );
+ 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,
+ });
+ },
+
+ // nsIObserver
+
+ observe(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(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) {
+ console.error(
+ "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 if (Services.prefs.getBoolPref("ldap_2.autoComplete.useDirectory")) {
+ // Try the global one
+ acDirURI = Services.prefs.getCharPref(
+ "ldap_2.autoComplete.directoryServer"
+ );
+ }
+
+ if (!acDirURI || Services.io.offline) {
+ // No directory to search or we are offline, 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 = Cc[
+ "@mozilla.org/addressbook/ldap-directory-query;1"
+ ].createInstance(Ci.nsIAbDirectoryQuery);
+ this._book = MailServices.ab
+ .getDirectory(acDirURI)
+ .QueryInterface(Ci.nsIAbLDAPDirectory);
+
+ // Create a minimal map just for the display name and primary email.
+ this._attributes = Cc[
+ "@mozilla.org/addressbook/ldap-attribute-map;1"
+ ].createInstance(Ci.nsIAbLDAPAttributeMap);
+ this._attributes.setAttributeList(
+ "DisplayName",
+ this._book.attributeMap.getAttributeList("DisplayName", {}),
+ true
+ );
+ this._attributes.setAttributeList(
+ "PrimaryEmail",
+ this._book.attributeMap.getAttributeList("PrimaryEmail", {}),
+ true
+ );
+ this._attributes.setAttributeList(
+ "SecondEmail",
+ this._book.attributeMap.getAttributeList("SecondEmail", {}),
+ true
+ );
+ }
+
+ this._result._commentColumn = this._book.dirName;
+ this._listener = aListener;
+ this._timer.init(this, 60000, Ci.nsITimer.TYPE_ONE_SHOT);
+
+ var args = Cc[
+ "@mozilla.org/addressbook/directory/query-arguments;1"
+ ].createInstance(Ci.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=*%v*)(givenName=*%v1*)(sn=*%v*))";
+ }
+
+ // Create filter from filter template and search string
+ var ldapSvc = Cc["@mozilla.org/network/ldap-service;1"].getService(
+ Ci.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() {
+ if (this._listener) {
+ this._query.stopQuery(this._context);
+ this._listener = null;
+ }
+ },
+
+ // nsIAbDirSearchListener
+
+ onSearchFinished(status, complete, secInfo, location) {
+ if (!this._listener) {
+ return;
+ }
+
+ if (status == Cr.NS_OK) {
+ if (this._result.matchCount) {
+ this._result.searchResult = ACR.RESULT_SUCCESS;
+ this._result.defaultIndex = 0;
+ } else {
+ this._result.searchResult = ACR.RESULT_NOMATCH;
+ }
+ } else {
+ 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(aCard) {
+ if (!this._listener) {
+ return;
+ }
+
+ for (let emailAddress of aCard.emailAddresses) {
+ this._addToResult(aCard, emailAddress);
+ }
+
+ /* 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: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsIAutoCompleteSearch",
+ "nsIAbDirSearchListener",
+ ]),
+};