mirror of
https://github.com/ManchildProductions/binoc-central-mirror.git
synced 2026-06-12 06:49:07 +00:00
558 lines
20 KiB
JavaScript
558 lines
20 KiB
JavaScript
/* 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/. */
|
|
|
|
this.EXPORTED_SYMBOLS = ['SearchSpec'];
|
|
|
|
var Cc = Components.classes;
|
|
var Ci = Components.interfaces;
|
|
var Cr = Components.results;
|
|
var Cu = Components.utils;
|
|
|
|
Cu.import("resource:///modules/iteratorUtils.jsm");
|
|
Cu.import("resource:///modules/mailServices.js");
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
|
|
var nsMsgSearchScope = Ci.nsMsgSearchScope;
|
|
var nsIMsgSearchTerm = Ci.nsIMsgSearchTerm;
|
|
var nsIMsgLocalMailFolder = Ci.nsIMsgLocalMailFolder;
|
|
var nsMsgFolderFlags = Ci.nsMsgFolderFlags;
|
|
var nsMsgSearchAttrib = Ci.nsMsgSearchAttrib;
|
|
|
|
var NS_MSG_SEARCH_INTERRUPTED = 0x00550002;
|
|
|
|
/**
|
|
* 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: function SearchSpec_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 =
|
|
Components.classes["@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: function SearchSpec_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);
|
|
}
|
|
}
|
|
// if it's synthetic but we have no search terms, hook the output of the
|
|
// synthetic view directly up to the search nsIMsgDBView
|
|
else if (this.owner.isSynthetic) {
|
|
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: function SearchSpec_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: function SearchSpec__flattenGroupifyTerms(aTerms,
|
|
aCloneTerms){
|
|
let iTerm = 0, term;
|
|
let outTerms = aCloneTerms ? [] : aTerms;
|
|
for (term in fixIterator(aTerms, Ci.nsIMsgSearchTerm)) {
|
|
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: function SearchSpec__groupifyTerms(aTerms, aCloneTerms) {
|
|
let term;
|
|
let outTerms = aCloneTerms ? [] : aTerms;
|
|
let inGroup = false;
|
|
for (term in fixIterator(aTerms, Components.interfaces.nsIMsgSearchTerm)) {
|
|
// 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;
|
|
}
|
|
// If we're not in a group, the boolean must be AND. It's okay for a group
|
|
// to start.
|
|
else {
|
|
// 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);
|
|
// if they are nulling out already null values, do not apply view changes!
|
|
else if (this._viewTerms === null)
|
|
return;
|
|
else
|
|
this._viewTerms = null;
|
|
this.owner._applyViewChanges();
|
|
},
|
|
/**
|
|
* @return 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);
|
|
// if they are nulling out already null values, do not apply view changes!
|
|
else if (this._virtualFolderTerms === null)
|
|
return;
|
|
else
|
|
this._virtualFolderTerms = null;
|
|
this.owner._applyViewChanges();
|
|
},
|
|
/**
|
|
* @return 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);
|
|
// if they are nulling out already null values, do not apply view changes!
|
|
else if (this._userTerms === null)
|
|
return;
|
|
else
|
|
this._userTerms = null;
|
|
this.owner._applyViewChanges();
|
|
},
|
|
/**
|
|
* @return 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: function SearchSpec_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: function SearchSpec_applySearch() {
|
|
let session = this.session;
|
|
|
|
// clear out our current terms and scope
|
|
session.searchTerms.QueryInterface(Ci.nsISupportsArray).Clear();
|
|
session.clearScopes();
|
|
|
|
// the scope logic needs to know if any terms look at the body attribute.
|
|
let haveBodyTerm = false;
|
|
|
|
// -- apply terms
|
|
if (this._virtualFolderTerms) {
|
|
for (let term in fixIterator(this._virtualFolderTerms,
|
|
nsIMsgSearchTerm)) {
|
|
if (term.attrib == nsMsgSearchAttrib.Body)
|
|
haveBodyTerm = true;
|
|
session.appendTerm(term);
|
|
}
|
|
}
|
|
|
|
if (this._viewTerms) {
|
|
for (let term in fixIterator(this._viewTerms,
|
|
nsIMsgSearchTerm)) {
|
|
if (term.attrib == nsMsgSearchAttrib.Body)
|
|
haveBodyTerm = true;
|
|
session.appendTerm(term);
|
|
}
|
|
}
|
|
|
|
if (this._userTerms) {
|
|
for (let term in fixIterator(this._userTerms,
|
|
nsIMsgSearchTerm)) {
|
|
if (term.attrib == nsMsgSearchAttrib.Body)
|
|
haveBodyTerm = true;
|
|
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(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 == nsMsgSearchScope.offlineMail) ||
|
|
(folder instanceof 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 & nsMsgFolderFlags.Offline)
|
|
offlineScope = nsMsgSearchScope.offlineMail;
|
|
else
|
|
// The onlineManual table is used for local search when there is no
|
|
// body available.
|
|
offlineScope = nsMsgSearchScope.onlineManual;
|
|
|
|
let offlineValidityTable = validityManager.getTable(offlineScope);
|
|
let offlineAvailable = true;
|
|
let onlineAvailable = true;
|
|
for (let term in fixIterator(session.searchTerms,
|
|
nsIMsgSearchTerm)) {
|
|
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?
|
|
Cu.reportError("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, unless we're
|
|
// filtering (quick search and/or a view selected)
|
|
if (onlineAvailable && offlineAvailable)
|
|
scope = (!filtering && this.onlineSearch) ? serverScope : offlineScope;
|
|
// If only one works, use it. Otherwise, default to offline
|
|
else if (onlineAvailable)
|
|
scope = serverScope;
|
|
else
|
|
scope = offlineScope;
|
|
}
|
|
session.addScopeTerm(scope, folder);
|
|
}
|
|
},
|
|
|
|
prettyStringOfSearchTerms: function(aSearchTerms) {
|
|
if (aSearchTerms == null)
|
|
return ' (none)\n';
|
|
|
|
let s = '';
|
|
|
|
for (let term in fixIterator(aSearchTerms, nsIMsgSearchTerm)) {
|
|
s += ' ' + term.termAsString + '\n';
|
|
}
|
|
|
|
return s;
|
|
},
|
|
|
|
prettyString: function() {
|
|
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: function SearchSpecListener_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) {
|
|
Cu.reportErrror("Search originated from unknown initiator! Confusion!");
|
|
this.searchSpec.owner.searching = true;
|
|
}
|
|
},
|
|
|
|
onSearchHit: function SearchSpecListener_onSearchHit(aMsgHdr, aFolder) {
|
|
// this method is never invoked!
|
|
},
|
|
|
|
onSearchDone: function SearchSpecListener_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: function FilteringSyntheticListener_onNewSearch() {
|
|
this.searchSpec.owner.searching = true;
|
|
this.dbView.onNewSearch();
|
|
},
|
|
onSearchHit:
|
|
function FilteringSyntheticListener_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: function FilteringSyntheticListener_OnSearchDone(aStatus) {
|
|
this.searchSpec.owner.searching = false;
|
|
this.dbView.onSearchDone(aStatus);
|
|
}
|
|
};
|