summaryrefslogtreecommitdiffstats
path: root/comm/mail/modules/SearchSpec.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/modules/SearchSpec.jsm')
-rw-r--r--comm/mail/modules/SearchSpec.jsm562
1 files changed, 562 insertions, 0 deletions
diff --git a/comm/mail/modules/SearchSpec.jsm b/comm/mail/modules/SearchSpec.jsm
new file mode 100644
index 0000000000..50bbfbaa64
--- /dev/null
+++ b/comm/mail/modules/SearchSpec.jsm
@@ -0,0 +1,562 @@
+/* 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/. */
+
+const EXPORTED_SYMBOLS = ["SearchSpec"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * Wrapper abstraction around a view's search session. This is basically a
+ * friend class of FolderDisplayWidget and is privy to some of its internals.
+ */
+function SearchSpec(aViewWrapper) {
+ this.owner = aViewWrapper;
+
+ this._viewTerms = null;
+ this._virtualFolderTerms = null;
+ this._userTerms = null;
+
+ this._session = null;
+ this._sessionListener = null;
+ this._listenersRegistered = false;
+
+ this._onlineSearch = false;
+}
+SearchSpec.prototype = {
+ /**
+ * Clone this SearchSpec; intended to be used by DBViewWrapper.clone().
+ */
+ clone(aViewWrapper) {
+ let doppel = new SearchSpec(aViewWrapper);
+
+ // we can just copy the terms since we never mutate them
+ doppel._viewTerms = this._viewTerms;
+ doppel._virtualFolderTerms = this._virtualFolderTerms;
+ doppel._userTerms = this._userTerms;
+
+ // _session can stay null
+ // no listener is required, so we can keep _sessionListener and
+ // _listenersRegistered at their default values
+
+ return doppel;
+ },
+
+ get hasSearchTerms() {
+ return this._viewTerms || this._virtualFolderTerms || this._userTerms;
+ },
+
+ get hasOnlyVirtualTerms() {
+ return this._virtualFolderTerms && !this._viewTerms && !this._userTerms;
+ },
+
+ /**
+ * On-demand creation of the nsIMsgSearchSession. Automatically creates a
+ * SearchSpecListener at the same time and registers it as a listener. The
+ * DBViewWrapper is responsible for adding (and removing) the db view
+ * as a listener.
+ *
+ * Code should only access this attribute when it wants to manipulate the
+ * session. Callers should use hasSearchTerms if they want to determine if
+ * a search session is required.
+ */
+ get session() {
+ if (this._session == null) {
+ this._session = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ }
+ return this._session;
+ },
+
+ /**
+ * (Potentially) add the db view as a search listener and kick off the search.
+ * We only do that if we have search terms. The intent is to allow you to
+ * call this all the time, even if you don't need to.
+ * DBViewWrapper._applyViewChanges used to handle a lot more of this, but our
+ * need to make sure that the session listener gets added after the DBView
+ * caused us to introduce this method. (We want the DB View's OnDone method
+ * to run before our listener, as it may do important work.)
+ */
+ associateView(aDBView) {
+ if (this.hasSearchTerms) {
+ this.updateSession();
+
+ if (this.owner.isSynthetic) {
+ this.owner._syntheticView.search(new FilteringSyntheticListener(this));
+ } else {
+ if (!this._sessionListener) {
+ this._sessionListener = new SearchSpecListener(this);
+ }
+
+ this.session.registerListener(
+ aDBView,
+ Ci.nsIMsgSearchSession.allNotifications
+ );
+ aDBView.searchSession = this._session;
+ this._session.registerListener(
+ this._sessionListener,
+ Ci.nsIMsgSearchSession.onNewSearch |
+ Ci.nsIMsgSearchSession.onSearchDone
+ );
+ this._listenersRegistered = true;
+
+ this.owner.searching = true;
+ this.session.search(this.owner.listener.msgWindow);
+ }
+ } else if (this.owner.isSynthetic) {
+ // If it's synthetic but we have no search terms, hook the output of the
+ // synthetic view directly up to the search nsIMsgDBView.
+ let owner = this.owner;
+ owner.searching = true;
+ this.owner._syntheticView.search(
+ aDBView.QueryInterface(Ci.nsIMsgSearchNotify),
+ function () {
+ owner.searching = false;
+ }
+ );
+ }
+ },
+ /**
+ * Stop any active search and stop the db view being a search listener (if it
+ * is one).
+ */
+ dissociateView(aDBView) {
+ // If we are currently searching, interrupt the search. This will
+ // immediately notify the listeners that the search is done with and
+ // clear the searching flag for us.
+ if (this.owner.searching) {
+ if (this.owner.isSynthetic) {
+ this.owner._syntheticView.abortSearch();
+ } else {
+ this.session.interruptSearch();
+ }
+ }
+
+ if (this._listenersRegistered) {
+ this._session.unregisterListener(this._sessionListener);
+ this._session.unregisterListener(aDBView);
+ aDBView.searchSession = null;
+ this._listenersRegistered = false;
+ }
+ },
+
+ /**
+ * Given a list of terms, mutate them so that they form a single boolean
+ * group.
+ *
+ * @param aTerms The search terms
+ * @param aCloneTerms Do we need to clone the terms?
+ */
+ _flattenGroupifyTerms(aTerms, aCloneTerms) {
+ let iTerm = 0,
+ term;
+ let outTerms = aCloneTerms ? [] : aTerms;
+ for (term of aTerms) {
+ if (aCloneTerms) {
+ let cloneTerm = this.session.createTerm();
+ cloneTerm.value = term.value;
+ cloneTerm.attrib = term.attrib;
+ cloneTerm.arbitraryHeader = term.arbitraryHeader;
+ cloneTerm.hdrProperty = term.hdrProperty;
+ cloneTerm.customId = term.customId;
+ cloneTerm.op = term.op;
+ cloneTerm.booleanAnd = term.booleanAnd;
+ cloneTerm.matchAll = term.matchAll;
+ term = cloneTerm;
+ outTerms.push(term);
+ }
+ if (iTerm == 0) {
+ term.beginsGrouping = true;
+ term.endsGrouping = false;
+ term.booleanAnd = true;
+ } else {
+ term.beginsGrouping = false;
+ term.endsGrouping = false;
+ }
+ iTerm++;
+ }
+ if (term) {
+ term.endsGrouping = true;
+ }
+
+ return outTerms;
+ },
+
+ /**
+ * Normalize the provided list of terms so that all of the 'groups' in it are
+ * ANDed together. If any OR clauses are detected outside of a group, we
+ * defer to |_flattenGroupifyTerms| to force the terms to be bundled up into
+ * a single group, maintaining the booleanAnd state of terms.
+ *
+ * This particular logic is desired because it allows the quick filter bar to
+ * produce interesting and useful filters.
+ *
+ * @param aTerms The search terms
+ * @param aCloneTerms Do we need to clone the terms?
+ */
+ _groupifyTerms(aTerms, aCloneTerms) {
+ let term;
+ let outTerms = aCloneTerms ? [] : aTerms;
+ let inGroup = false;
+ for (term of aTerms) {
+ // If we're in a group, all that is forbidden is the creation of new
+ // groups.
+ if (inGroup) {
+ if (term.beginsGrouping) {
+ // forbidden!
+ return this._flattenGroupifyTerms(aTerms, aCloneTerms);
+ } else if (term.endsGrouping) {
+ inGroup = false;
+ }
+ } else {
+ // If we're not in a group, the boolean must be AND. It's okay for a group
+ // to start.
+ // If it's not an AND then it needs to be in a group and we use the other
+ // function to take care of it. (This function can't back up...)
+ if (!term.booleanAnd) {
+ return this._flattenGroupifyTerms(aTerms, aCloneTerms);
+ }
+
+ inGroup = term.beginsGrouping;
+ }
+
+ if (aCloneTerms) {
+ let cloneTerm = this.session.createTerm();
+ cloneTerm.attrib = term.attrib;
+ cloneTerm.value = term.value;
+ cloneTerm.arbitraryHeader = term.arbitraryHeader;
+ cloneTerm.hdrProperty = term.hdrProperty;
+ cloneTerm.customId = term.customId;
+ cloneTerm.op = term.op;
+ cloneTerm.booleanAnd = term.booleanAnd;
+ cloneTerm.matchAll = term.matchAll;
+ cloneTerm.beginsGrouping = term.beginsGrouping;
+ cloneTerm.endsGrouping = term.endsGrouping;
+ term = cloneTerm;
+ outTerms.push(term);
+ }
+ }
+
+ return outTerms;
+ },
+
+ /**
+ * Set search terms that are defined by the 'view', which translates to that
+ * weird combo-box that lets you view your unread messages, messages by tag,
+ * messages that aren't deleted, etc.
+ *
+ * @param aViewTerms The list of terms. We take ownership and mutate it.
+ */
+ set viewTerms(aViewTerms) {
+ if (aViewTerms) {
+ this._viewTerms = this._groupifyTerms(aViewTerms);
+ } else if (this._viewTerms === null) {
+ // If they are nulling out already null values, do not apply view changes!
+ return;
+ } else {
+ this._viewTerms = null;
+ }
+ this.owner._applyViewChanges();
+ },
+ /**
+ * @returns the view terms currently in effect. Do not mutate this.
+ */
+ get viewTerms() {
+ return this._viewTerms;
+ },
+ /**
+ * Set search terms that are defined by the 'virtual folder' definition. This
+ * could also be thought of as the 'saved search' part of a saved search.
+ *
+ * @param aVirtualFolderTerms The list of terms. We make our own copy and
+ * do not mutate yours.
+ */
+ set virtualFolderTerms(aVirtualFolderTerms) {
+ if (aVirtualFolderTerms) {
+ // we need to clone virtual folder terms because they are pulled from a
+ // persistent location rather than created on demand
+ this._virtualFolderTerms = this._groupifyTerms(aVirtualFolderTerms, true);
+ } else if (this._virtualFolderTerms === null) {
+ // If they are nulling out already null values, do not apply view changes!
+ return;
+ } else {
+ this._virtualFolderTerms = null;
+ }
+ this.owner._applyViewChanges();
+ },
+ /**
+ * @returns the Virtual folder terms currently in effect. Do not mutate this.
+ */
+ get virtualFolderTerms() {
+ return this._virtualFolderTerms;
+ },
+
+ /**
+ * Set the terms that the user is explicitly searching on. These will be
+ * augmented with the 'context' search terms potentially provided by
+ * viewTerms and virtualFolderTerms.
+ *
+ * @param aUserTerms The list of terms. We take ownership and mutate it.
+ */
+ set userTerms(aUserTerms) {
+ if (aUserTerms) {
+ this._userTerms = this._groupifyTerms(aUserTerms);
+ } else if (this._userTerms === null) {
+ // If they are nulling out already null values, do not apply view changes!
+ return;
+ } else {
+ this._userTerms = null;
+ }
+ this.owner._applyViewChanges();
+ },
+ /**
+ * @returns the user terms currently in effect as set via the |userTerms|
+ * attribute or via the |quickSearch| method. Do not mutate this.
+ */
+ get userTerms() {
+ return this._userTerms;
+ },
+
+ clear() {
+ if (this.hasSearchTerms) {
+ this._viewTerms = null;
+ this._virtualFolderTerms = null;
+ this._userTerms = null;
+ this.owner._applyViewChanges();
+ }
+ },
+
+ get onlineSearch() {
+ return this._onlineSearch;
+ },
+ /**
+ * Virtual folders have a concept of 'online search' which affects the logic
+ * in updateSession that builds our search scopes. If onlineSearch is false,
+ * then when displaying the virtual folder unaffected by mail views or quick
+ * searches, we will most definitely perform an offline search. If
+ * onlineSearch is true, we will perform an online search only for folders
+ * which are not available offline and for which the server is configured
+ * to have an online 'searchScope'.
+ * When mail views or quick searches are in effect our search is always
+ * offline unless the only way to satisfy the needs of the constraints is an
+ * online search (read: the message body is required but not available
+ * offline.)
+ */
+ set onlineSearch(aOnlineSearch) {
+ this._onlineSearch = aOnlineSearch;
+ },
+
+ /**
+ * Populate the search session using viewTerms, virtualFolderTerms, and
+ * userTerms. The way this works is that each of the 'context' sets of
+ * terms gets wrapped into a group which is boolean anded together with
+ * everything else.
+ */
+ updateSession() {
+ let session = this.session;
+
+ // clear out our current terms and scope
+ session.searchTerms = [];
+ session.clearScopes();
+
+ // -- apply terms
+ if (this._virtualFolderTerms) {
+ for (let term of this._virtualFolderTerms) {
+ session.appendTerm(term);
+ }
+ }
+
+ if (this._viewTerms) {
+ for (let term of this._viewTerms) {
+ session.appendTerm(term);
+ }
+ }
+
+ if (this._userTerms) {
+ for (let term of this._userTerms) {
+ session.appendTerm(term);
+ }
+ }
+
+ // -- apply scopes
+ // If it is a synthetic view, create a single bogus scope so that we can use
+ // MatchHdr.
+ if (this.owner.isSynthetic) {
+ // We don't want to pass in a folder, and we don't want to use the
+ // allSearchableGroups scope, so we cheat and use AddDirectoryScopeTerm.
+ session.addDirectoryScopeTerm(Ci.nsMsgSearchScope.offlineMail);
+ return;
+ }
+
+ let filtering = this._userTerms != null || this._viewTerms != null;
+ let validityManager = Cc[
+ "@mozilla.org/mail/search/validityManager;1"
+ ].getService(Ci.nsIMsgSearchValidityManager);
+ for (let folder of this.owner._underlyingFolders) {
+ // we do not need to check isServer here because _underlyingFolders
+ // filtered it out when it was initialized.
+
+ let scope;
+ let serverScope = folder.server.searchScope;
+ // If we're offline, or this is a local folder, or there's no separate
+ // online scope, use server scope.
+ if (
+ Services.io.offline ||
+ serverScope == Ci.nsMsgSearchScope.offlineMail ||
+ folder instanceof Ci.nsIMsgLocalMailFolder
+ ) {
+ scope = serverScope;
+ } else {
+ // we need to test the validity in online and offline tables
+ let onlineValidityTable = validityManager.getTable(serverScope);
+
+ let offlineScope;
+ if (folder.flags & Ci.nsMsgFolderFlags.Offline) {
+ offlineScope = Ci.nsMsgSearchScope.offlineMail;
+ } else {
+ // The onlineManual table is used for local search when there is no
+ // body available.
+ offlineScope = Ci.nsMsgSearchScope.onlineManual;
+ }
+
+ let offlineValidityTable = validityManager.getTable(offlineScope);
+ let offlineAvailable = true;
+ let onlineAvailable = true;
+ for (let term of session.searchTerms) {
+ if (!term.matchAll) {
+ // for custom terms, we need to getAvailable from the custom term
+ if (term.attrib == Ci.nsMsgSearchAttrib.Custom) {
+ let customTerm = MailServices.filters.getCustomTerm(
+ term.customId
+ );
+ if (customTerm) {
+ offlineAvailable = customTerm.getAvailable(
+ offlineScope,
+ term.op
+ );
+ onlineAvailable = customTerm.getAvailable(serverScope, term.op);
+ } else {
+ // maybe an extension with a custom term was unloaded?
+ console.error(
+ "Custom search term " + term.customId + " missing"
+ );
+ }
+ } else {
+ if (!offlineValidityTable.getAvailable(term.attrib, term.op)) {
+ offlineAvailable = false;
+ }
+ if (!onlineValidityTable.getAvailable(term.attrib, term.op)) {
+ onlineAvailable = false;
+ }
+ }
+ }
+ }
+ // If both scopes work, honor the onlineSearch request, for saved search folders (!filtering)
+ // and the search dialog (!displayedFolder).
+ // If only one works, use it. Otherwise, default to offline
+ if (onlineAvailable && offlineAvailable) {
+ scope =
+ (!filtering || !this.owner.displayedFolder) && this.onlineSearch
+ ? serverScope
+ : offlineScope;
+ } else if (onlineAvailable) {
+ scope = serverScope;
+ } else {
+ scope = offlineScope;
+ }
+ }
+ session.addScopeTerm(scope, folder);
+ }
+ },
+
+ prettyStringOfSearchTerms(aSearchTerms) {
+ if (aSearchTerms == null) {
+ return " (none)\n";
+ }
+
+ let s = "";
+
+ for (let term of aSearchTerms) {
+ s += " " + term.termAsString + "\n";
+ }
+
+ return s;
+ },
+
+ prettyString() {
+ let s = " Search Terms:\n";
+ s += " Virtual Folder Terms:\n";
+ s += this.prettyStringOfSearchTerms(this._virtualFolderTerms);
+ s += " View Terms:\n";
+ s += this.prettyStringOfSearchTerms(this._viewTerms);
+ s += " User Terms:\n";
+ s += this.prettyStringOfSearchTerms(this._userTerms);
+ s += " Scope (Folders):\n";
+ for (let folder of this.owner._underlyingFolders) {
+ s += " " + folder.prettyName + "\n";
+ }
+ return s;
+ },
+};
+
+/**
+ * A simple nsIMsgSearchNotify listener that only listens for search start/stop
+ * so that it can tell the DBViewWrapper when the search has completed.
+ */
+function SearchSpecListener(aSearchSpec) {
+ this.searchSpec = aSearchSpec;
+}
+SearchSpecListener.prototype = {
+ onNewSearch() {
+ // searching should already be true by the time this happens. if it's not,
+ // it means some code is poking at the search session. bad!
+ if (!this.searchSpec.owner.searching) {
+ console.error("Search originated from unknown initiator! Confusion!");
+ this.searchSpec.owner.searching = true;
+ }
+ },
+
+ onSearchHit(aMsgHdr, aFolder) {
+ // this method is never invoked!
+ },
+
+ onSearchDone(aStatus) {
+ this.searchSpec.owner.searching = false;
+ },
+};
+
+/**
+ * Pretend to implement the nsIMsgSearchNotify interface, checking all matches
+ * we are given against the search session on the search spec. If they pass,
+ * relay them to the underlying db view, otherwise quietly eat them.
+ * This is what allows us to use mail-views and quick searches against
+ * gloda-backed searches.
+ */
+function FilteringSyntheticListener(aSearchSpec) {
+ this.searchSpec = aSearchSpec;
+ this.session = this.searchSpec.session;
+ this.dbView = this.searchSpec.owner.dbView.QueryInterface(
+ Ci.nsIMsgSearchNotify
+ );
+}
+FilteringSyntheticListener.prototype = {
+ onNewSearch() {
+ this.searchSpec.owner.searching = true;
+ this.dbView.onNewSearch();
+ },
+ onSearchHit(aMsgHdr, aFolder) {
+ // We don't need to worry about msgDatabase opening the database.
+ // It is (obviously) already open, and presumably gloda is already on the
+ // hook to perform the cleanup (assuming gloda is backing this search).
+ if (this.session.MatchHdr(aMsgHdr, aFolder.msgDatabase)) {
+ this.dbView.onSearchHit(aMsgHdr, aFolder);
+ }
+ },
+ onSearchDone(aStatus) {
+ this.searchSpec.owner.searching = false;
+ this.dbView.onSearchDone(aStatus);
+ },
+};