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
const { MailServices } = ChromeUtils.importESModule(
);
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
ChromeUtils.defineESModuleGetters(
lazy,
{
},
{ global: "current" }
);
ChromeUtils.defineLazyGetter(lazy, "messengerBundle", () =>
Services.strings.createBundle(
)
);
/**
* Opens the dialog to create a new sub-folder, and creates it if the user
* accepts.
*
* @param {nsIMsgFolder} folder - The parent for the new subfolder.
*/
function newFolder(folder) {
// Make sure we actually can create subfolders.
if (!folder.canCreateSubfolders) {
// Check if we can create them at the root, otherwise use the default
// account as root folder.
const rootMsgFolder = folder.server.rootMsgFolder;
folder = rootMsgFolder.canCreateSubfolders
? rootMsgFolder
: top.GetDefaultAccountRootFolder();
}
let dualUseFolders = true;
if (folder.server instanceof Ci.nsIImapIncomingServer) {
dualUseFolders = folder.server.dualUseFolders;
}
top.openDialog(
"",
"chrome,modal,resizable=no,centerscreen",
{ folder, dualUseFolders, okCallback: newFolderOkCallback }
);
}
/**
* Callback executed when the user selects OK in the create folder dialog.
*
* @param {string} subfolderName
* @param {nsIMsgFolder} parentFolder
*/
async function newFolderOkCallback(subfolderName, parentFolder) {
// TODO: Rewrite this logic and also move the opening of alert dialogs from
if (!subfolderName) {
return;
}
const promiseNewFolder = new Promise(resolve => {
const listener = {
folderAdded: addedFolder => {
if (addedFolder.localizedName == subfolderName) {
MailServices.mfn.removeListener(listener);
resolve(addedFolder);
}
},
};
MailServices.mfn.addListener(
listener,
Ci.nsIMsgFolderNotificationService.folderAdded
);
});
parentFolder.createSubfolder(subfolderName, top.msgWindow);
const folder = await promiseNewFolder;
if (!parentFolder.isServer) {
// Inherit view/sort/columns from parent folder.
const parentInfo = parentFolder.msgDatabase.dBFolderInfo;
const newInfo = folder.msgDatabase.dBFolderInfo;
newInfo.viewFlags = parentInfo.viewFlags;
newInfo.sortType = parentInfo.sortType;
newInfo.sortOrder = parentInfo.sortOrder;
newInfo.setCharProperty(
"columnStates",
parentInfo.getCharProperty("columnStates")
);
}
folder.updateTimestamps(true);
}
/**
* Opens the dialog to create a new virtual folder
*
* @param {string} defaultName - The default name for the new folder.
* @param {nsIMsgSearchTerm[]} searchTerms - The search terms associated
* with the folder.
* @param {nsIMsgFolder} parentFolder - The folder to run the search terms on.
*/
function newVirtualFolder(defaultName, searchTerms, parentFolder) {
const folder = parentFolder || top.GetDefaultAccountRootFolder();
if (!folder) {
return;
}
let newFolderName = folder.localizedName;
if (defaultName) {
newFolderName += "-" + defaultName;
}
top.openDialog(
"",
"chrome,modal,centerscreen,resizable=yes",
{ folder, searchTerms, newFolderName }
);
}
/**
* Opens the dialog to rename a particular folder, and does the renaming if
* the user clicks OK in that dialog
*
* @param {nsIMsgFolder} folder - The folder to rename.
*/
function renameFolder(folder) {
function renameCallback(newName, uri) {
if (uri != folder.URI) {
console.error("got back a different folder to rename!");
}
// Actually do the rename.
folder.rename(newName, top.msgWindow);
}
top.openDialog(
"",
"chrome,modal,centerscreen",
{
preselectedURI: folder.URI,
okCallback: renameCallback,
name: folder.localizedName,
}
);
}
/**
* Opens the dialog to edit the properties of a folder.
*
* @param {nsIMsgFolder} folder - The folder to edit.
* @param {string} [tabID] - Id of initial tab to select in the folder
* properties dialog.
*/
function editFolder(folder, tabID) {
if (folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) {
editVirtualFolder(folder);
return;
}
const title = lazy.messengerBundle.GetStringFromName("folderProperties");
// If the main window has been closed by the user, make sure that the
// folder properties dialog is removed as well,
let folderPropertiesDialog = null;
const onMainWindowUnload = () => {
folderPropertiesDialog.close();
};
window.addEventListener("unload", onMainWindowUnload);
// Save the focus and freeze the about3Pane.
const prevFocusedElement = document.activeElement;
document.documentElement.setAttribute("inert", "true");
function editFolderCallback(newName, oldName) {
if (newName != oldName) {
folder.rename(newName, top.msgWindow);
}
}
function unloadDialogCallback() {
// Unfreeze about3Pane and restore focus.
document.documentElement.removeAttribute("inert");
prevFocusedElement?.focus();
window.removeEventListener("unload", onMainWindowUnload);
}
folderPropertiesDialog = top.openDialog(
"",
"chrome,dependent,centerscreen",
{
folder,
serverType: folder.server.type,
msgWindow: top.msgWindow,
title,
okCallback: editFolderCallback,
tabID,
name: folder.localizedName,
rebuildSummaryCallback: this.rebuildFolderSummary,
unloadCallback: unloadDialogCallback,
}
);
}
/**
* Opens the dialog to edit the properties of a folder.
*
* @param {nsIMsgFolder} folder
*/
function editVirtualFolder(folder) {
function editVirtualCallback() {
if (window.gFolder == folder) {
window.folderTree.dispatchEvent(new CustomEvent("select"));
}
}
top.openDialog(
"",
"chrome,modal,centerscreen,resizable=yes",
{
folder,
editExistingFolder: true,
onOKCallback: editVirtualCallback,
msgWindow: top.msgWindow,
}
);
}
async function rebuildFolderSummary(folder) {
if (folder.locked) {
folder.throwAlertMsg("operationFailedFolderBusy", top.msgWindow);
return;
}
if (folder.supportsOffline) {
// Remove the offline store, if any.
await IOUtils.remove(folder.filePath.path, { recursive: true }).catch(
console.error
);
} else if (
Services.prefs.getCharPref(
`mail.server.${folder.server.key}.storeContractID`
) == "@mozilla.org/msgstore/berkeleystore;1"
) {
// For local mbox, fix classic MacOS line endings.
try {
folder.acquireSemaphore(folder, "folderPane.rebuildFolderSummary");
await lazy.repairMbox(folder.filePath.path);
} catch (e) {
console.warn(`Repair mbox FAILED; ${e.message}`);
} finally {
folder.releaseSemaphore(folder, "folderPane.rebuildFolderSummary");
}
}
// The following notification causes all DBViewWrappers that include
// this folder to rebuild their views.
MailServices.mfn.notifyFolderReindexTriggered(folder);
folder.msgDatabase.summaryValid = false;
try {
let transferInfo = null;
switch (folder.server.type) {
case "imap":
transferInfo = folder.dBTransferInfo.QueryInterface(
Ci.nsIWritablePropertyBag2
);
transferInfo.setPropertyAsACString("numMsgs", "0");
transferInfo.setPropertyAsACString("numNewMsgs", "0");
// Reset UID validity so that nsImapMailFolder::UpdateImapMailboxInfo
// will recognize that a folder repair is in progress.
transferInfo.setPropertyAsACString("UIDValidity", "-1"); // == kUidUnknown
break;
case "ews":
case "graph":
// Reset the sync state token so that the next sync will download the
// message list again.
folder.setStringProperty("ewsSyncStateToken", "");
break;
}
folder.closeAndBackupFolderDB("");
if (folder.server.type == "imap" && transferInfo) {
folder.dBTransferInfo = transferInfo;
}
} catch (e) {
// In a failure, proceed anyway since we're dealing with problems
folder.ForceDBClosed();
}
// The local store was deleted above. It won't be recreated until the user
// attempts to load a message or the offline sync process creates it.
// However, folder discovery relies on the existence of the offline store,
// so to avoid an intermediate state that could cause folder discovery to
// fail for this folder, we create the local store.
folder.msgStore.ensureLocalStore(folder);
folder.updateFolder(top.msgWindow);
}
/**
* Deletes a folder from its parent. Also handles unsubscribe from newsgroups
* if the selected folder/s happen to be nntp.
*
* @param {nsIMsgFolder} folder - The folder to delete.
*/
function deleteFolder(folder) {
// For newsgroups, "delete" means "unsubscribe".
if (
folder.server.type == "nntp" &&
!folder.getFlag(Ci.nsMsgFolderFlags.Virtual)
) {
lazy.SubscribeCommands.MsgUnsubscribe(folder);
return;
}
if (!folder.deletable) {
throw new Error("Can't delete folder: " + folder.localizedName);
}
if (folder.getFlag(Ci.nsMsgFolderFlags.Virtual)) {
const confirmation = lazy.messengerBundle.GetStringFromName(
"confirmSavedSearchDeleteMessage"
);
const title = lazy.messengerBundle.GetStringFromName(
"confirmSavedSearchTitle"
);
if (
Services.prompt.confirmEx(
top,
title,
confirmation,
Services.prompt.STD_YES_NO_BUTTONS +
Services.prompt.BUTTON_POS_1_DEFAULT,
"",
"",
"",
"",
{}
) != 0
) {
/* the yes button is in position 0 */
return;
}
}
try {
folder.deleteSelf(top.msgWindow);
} catch (ex) {
// Ignore known errors from canceled warning dialogs.
const NS_MSG_ERROR_COPY_FOLDER_ABORTED = 0x8055001a;
if (ex.result != NS_MSG_ERROR_COPY_FOLDER_ABORTED) {
if (ex.result == Cr.NS_ERROR_FILE_NO_DEVICE_SPACE) {
// folder could not be deleted due to low space
// outOfDiskSpace message is too restricted to downloading
// operation so we created a new generic message, outOfDiskSpaceGeneric
folder.throwAlertMsg("outOfDiskSpaceGeneric", top.msgWindow);
} else {
throw ex;
}
}
}
}
/**
* Prompts for confirmation, if the user hasn't already chosen the "don't ask
* again" option.
*
* @param {string} aCommand - The command to prompt for.
* @param {nsIMsgFolder} aFolder - The folder for which the confirmation is requested.
* @returns {boolean}
*/
async function _checkConfirmationPrompt(aCommand, aFolder) {
// If no folder was specified, reject the operation.
if (!aFolder) {
return false;
}
const showPrompt = !Services.prefs.getBoolPref(
"mailnews." + aCommand + ".dontAskAgain",
false
);
if (!showPrompt) {
return true;
}
const [title, message, check] = await document.l10n.formatValues([
{
id: "prompt-empty-folder-title",
args: { folder: aFolder.localizedName },
},
{
id: "prompt-empty-folder-message",
args: { folder: aFolder.localizedName },
},
{ id: "prompt-dont-ask-again" },
]);
const checkbox = { value: false };
const response =
Services.prompt.confirmEx(
top,
title,
message,
Services.prompt.STD_YES_NO_BUTTONS,
null,
null,
null,
check,
checkbox
) == 0;
if (checkbox.value) {
Services.prefs.setBoolPref("mailnews." + aCommand + ".dontAskAgain", true);
}
return response;
}
/**
* Prompts the user to confirm and empties the trash for the selected folder.
* The folder and its children are only emptied if it has the proper Trash flag.
*
* @param {nsIMsgFolder} folder - The trash folder to empty.
*/
async function emptyTrash(folder) {
if (!folder.getFlag(Ci.nsMsgFolderFlags.Trash)) {
folder = folder.rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Trash);
}
if (!folder) {
return;
}
const confirmEmptyTrash = await _checkConfirmationPrompt(
"emptyTrash",
folder
);
if (!confirmEmptyTrash) {
return;
}
// Check if this is a top-level smart folder. If so, we're going
// to empty all the trash folders.
if (lazy.FolderUtils.isSmartVirtualFolder(folder)) {
for (const server of MailServices.accounts.allServers) {
for (const trash of server.rootFolder.getFoldersWithFlags(
Ci.nsMsgFolderFlags.Trash
)) {
trash.emptyTrash(null);
}
}
} else {
folder.emptyTrash(null);
}
}
/**
* Deletes everything (folders and messages) in the selected folder.
* The folder is only emptied if it has the proper Junk flag.
*
* @param {nsIMsgFolder} folder - The folder to empty.
* @param {boolean} [shouldPrompt=true] - If the user should be prompted.
*/
async function emptyJunk(folder, shouldPrompt = true) {
if (!folder || !folder.getFlag(Ci.nsMsgFolderFlags.Junk)) {
return;
}
// If prompt is true, ask the user to confirm. Don't want to fire this
// within recursive calls.
if (shouldPrompt) {
const confirmEmptyJunk = await _checkConfirmationPrompt(
"emptyJunk",
folder
);
if (!confirmEmptyJunk) {
return;
}
}
if (lazy.FolderUtils.isSmartVirtualFolder(folder)) {
// Unified junk folder: recurse through each real junk folder without prompting again.
const wrappedFolder = lazy.VirtualFolderHelper.wrapVirtualFolder(folder);
for (const searchFolder of wrappedFolder.searchFolders) {
this.emptyJunk(searchFolder, false);
}
return;
}
// Delete any subfolders this folder might have
for (const subFolder of folder.subFolders) {
folder.propagateDelete(subFolder, true);
}
const messages = [...folder.messages];
if (!messages.length) {
return;
}
// Now delete the messages
folder.deleteMessages(messages, top.msgWindow, true, false, null, false);
}
/**
* Compacts the given folder.
*
* @param {nsIMsgFolder} folder
*/
function compactFolder(folder) {
// Can't compact folders that have just been compacted.
if (folder.server.type != "imap" && !folder.expungedBytes) {
return;
}
folder.compact(null, top.msgWindow);
}
/**
* Compacts all folders for the account that the given folder belongs to.
*
* @param {nsIMsgFolder} folder
*/
function compactAllFoldersForAccount(folder) {
folder.rootFolder.compactAll(null, top.msgWindow);
}
export const FolderCommands = {
newFolder,
newVirtualFolder,
renameFolder,
editFolder,
rebuildFolderSummary,
deleteFolder,
emptyTrash,
emptyJunk,
compactFolder,
compactAllFoldersForAccount,
};