Revision control
Copy as Markdown
Other Tools
/* 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
/*
* Wrap everything about virtual folders.
*/
const WrapperConstructor = Components.Constructor(
"@mozilla.org/mailnews/virtual-folder-wrapper;1",
"nsIVirtualFolderWrapper"
);
export var VirtualFolderHelper = {
/**
* Create a new virtual folder (an actual nsIMsgFolder that did not previously
* exist), wrapping it in a VirtualFolderWrapper, and returning that wrapper.
*
* If the call to addSubfolder fails (and therefore throws), we will NOT catch
* it.
*
* @param {string} aFolderName - The name of the new folder to create.
* @param {nsIMsgFolder} aParentFolder - The folder in which to create the
* search folder.
* @param {nsIMsgFolder[]} aSearchFolders A list of nsIMsgFolders that you
* want to use as the sources for the virtual folder OR a string that is
* the already '|' delimited list of folder URIs to use.
* @param {nsIMsgSearchTerms[]} aSearchTerms - The search terms to
* use for the virtual folder.
* @param {boolean} aOnlineSearch Should the search attempt to use the
* server's search capabilities when possible and appropriate?
* @returns {VirtualFolderWrapper} The VirtualFolderWrapper wrapping the
* newly created folder. You would probably only want this for its
* virtualFolder attribute which has the nsIMsgFolder we created.
* Be careful about accessing any of the other attributes, as they will
* bring its message database back to life.
*/
createNewVirtualFolder(
aFolderName,
aParentFolder,
aSearchFolders,
aSearchTerms,
aOnlineSearch
) {
const msgFolder = aParentFolder.addSubfolder(aFolderName);
msgFolder.setFlag(Ci.nsMsgFolderFlags.Virtual);
const wrapped = new WrapperConstructor();
wrapped.virtualFolder = msgFolder;
if (typeof aSearchTerms == "string") {
wrapped.searchString = aSearchTerms;
} else {
wrapped.searchTerms = aSearchTerms;
}
if (typeof aSearchFolders == "string") {
aSearchFolders = aSearchFolders
.split("|")
.map(uri => MailServices.folderLookup.getFolderForURL(uri));
}
wrapped.searchFolders = aSearchFolders;
wrapped.onlineSearch = aOnlineSearch;
const msgDatabase = msgFolder.msgDatabase;
msgDatabase.summaryValid = true;
msgDatabase.close(true);
aParentFolder.notifyFolderAdded(msgFolder);
MailServices.accounts.saveVirtualFolders();
return wrapped;
},
/**
* Given an existing nsIMsgFolder that is a virtual folder, wrap it into a
* VirtualFolderWrapper.
*
* @param {nsIMsgFolder} aMsgFolder - The folder to use.
*/
wrapVirtualFolder(aMsgFolder) {
const wrapped = new WrapperConstructor();
wrapped.virtualFolder = aMsgFolder;
return wrapped;
},
};
/**
* Abstracts dealing with the properties of a virtual folder that differentiate
* it from a non-virtual folder. A virtual folder is an odd duck. When
* holding an nsIMsgFolder that is a virtual folder, it is distinguished by
* the virtual flag and a number of properties that tell us the string
* representation of its search, the folders it searches over, and whether we
* use online searching or not.
* Virtual folders and their defining attributes are loaded from
* virtualFolders.dat (in the profile directory) by the account manager at
* startup, (re-)creating them if need be. It also saves them back to the
* file at shutdown. The most important thing the account manager does is to
* create VirtualFolderChangeListener instances that are registered with the
* message database service. This means that if one of the databases for the
* folders that the virtual folder includes is opened for some reason (for
* example, new messages are added to the folder because of a filter or they
* are delivered there), the virtual folder gets a chance to know about this
* and update the virtual folder's "cache" of information, such as the message
* counts or the presence of the message in the folder.
* The odd part is that a lot of the virtual folder logic also happens as a
* result of the nsMsgDBView subclasses being told the search query and the
* underlying folders. This makes for an odd collaboration of UI and backend
* logic.
*
* Justification for this class: Virtual folders aren't all that complex, but
* they are complex enough that we don't want to have the same code duplicated
* all over the place. We also don't want to have a loose assembly of global
* functions for working with them. So here we are.
*
* Important! Accessing any of our attributes results in the message database
* being loaded so that we can access the dBFolderInfo associated with the
* database. The message database is not automatically forgotten by the
* folder, which can lead to an (effective) memory leak. Please make sure
* that you are playing your part in not leaking memory by only using the
* wrapper when you have a serious need to access the database, and by
* forcing the folder to forget about the database when you are done by
* setting the database to null (unless you know with confidence someone else
* definitely wants the database around and will clean it up.)
*
* @implements {nsIVirtualFolderWrapper}
*/
export class VirtualFolderWrapper {
QueryInterface = ChromeUtils.generateQI(["nsIVirtualFolderWrapper"]);
/**
* The virtual folder this wrapper wraps.
*
* @type {nsIMsgFolder}
*/
virtualFolder;
/**
* The list of nsIMsgFolders that this virtual folder is a search over.
*
* @type {nsIMsgFolders[]}
*/
get searchFolders() {
return this.#dbFolderInfo
.getCharProperty("searchFolderUri")
.split("|")
.map(s => s.trim())
.filter(Boolean)
.sort() // Put folders in URI order so a parent is always before a child.
.map(uri => {
try {
return MailServices.folderLookup.getOrCreateFolderForURL(uri);
} catch {
return null;
}
})
.filter(Boolean);
}
set searchFolders(folders) {
if (typeof folders == "string") {
this.#dbFolderInfo.setCharProperty("searchFolderUri", folders);
} else {
const uris = folders.map(folder => folder.URI);
this.#dbFolderInfo.setCharProperty("searchFolderUri", uris.join("|"));
}
Services.obs.notifyObservers(this.virtualFolder, "search-folders-changed");
}
/**
* A "|"-delimited string containing the URIs of the folders that back this
* virtual folder.
*
* @type {string}
*/
get searchFolderURIs() {
return this.#dbFolderInfo.getCharProperty("searchFolderUri");
}
/**
* A newly created filter with the search terms loaded into it that define
* this virtual folder. The filter is apparently useful as an
* `nsIMsgSearchSession` stand-in to some code.
*
* @type {nsIMsgFilter}
*/
get searchTermsSession() {
// Temporary means it doesn't get exposed to the UI and doesn't get saved to
// disk. Which is good, because this is just a trick to parse the string
// into search terms.
const filterList = MailServices.filters.getTempFilterList(
this.virtualFolder
);
const tempFilter = filterList.createFilter("temp");
filterList.parseCondition(tempFilter, this.searchString);
return tempFilter;
}
/**
* The list of search terms that define this virtual folder.
*
* @type {nsIMsgSearchTerm[]}
*/
get searchTerms() {
return this.searchTermsSession.searchTerms;
}
set searchTerms(terms) {
let condition = "";
for (const term of terms) {
if (condition) {
condition += " ";
}
if (term.matchAll) {
condition = "ALL";
break;
}
condition += term.booleanAnd ? "AND (" : "OR (";
condition += term.termAsString + ")";
}
this.searchString = condition;
}
/**
* The set of search terms that define this virtual folder as a string.
* You may prefer to use `searchTerms` which uses `nsIMsgSearchTerm` instead.
*
* @type {string}
*/
get searchString() {
return this.#dbFolderInfo.getCharProperty("searchStr");
}
set searchString(searchString) {
this.#dbFolderInfo.setCharProperty("searchStr", searchString);
}
/**
* Whether the virtual folder is configured for online search.
*
* @type {boolean}
*/
get onlineSearch() {
return this.#dbFolderInfo.getBooleanProperty("searchOnline", false);
}
set onlineSearch(onlineSearch) {
this.#dbFolderInfo.setBooleanProperty("searchOnline", onlineSearch);
}
/**
* The dBFolderInfo associated with the virtual folder directly. May be null.
* Will cause the message database to be opened, which may have memory
* bloat/leak ramifications, so make sure the folder's database was already
* going to be opened anyways or that you call `cleanUpMessageDatabase`.
*
* @type {?nsIDBFolderInfo}
*/
get #dbFolderInfo() {
const msgDatabase = this.virtualFolder.msgDatabase;
return msgDatabase && msgDatabase.dBFolderInfo;
}
/**
* Avoid memory bloat by making the virtual folder forget about its database.
* If the database is actually in use (read: someone is keeping it alive by
* having references to it from places other than the `nsIMsgFolder`), the
* folder will be able to re-establish the reference for minimal cost.
*/
cleanUpMessageDatabase() {
this.virtualFolder.msgDatabase.close(true);
this.virtualFolder.msgDatabase = null;
}
}