/* 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", ]), };