diff options
Diffstat (limited to 'comm/mailnews/search/content/FilterEditor.js')
-rw-r--r-- | comm/mailnews/search/content/FilterEditor.js | 809 |
1 files changed, 809 insertions, 0 deletions
diff --git a/comm/mailnews/search/content/FilterEditor.js b/comm/mailnews/search/content/FilterEditor.js new file mode 100644 index 0000000000..3b93773192 --- /dev/null +++ b/comm/mailnews/search/content/FilterEditor.js @@ -0,0 +1,809 @@ +/* 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/. */ + +/* import-globals-from searchTerm.js */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); +var { PluralForm } = ChromeUtils.importESModule( + "resource://gre/modules/PluralForm.sys.mjs" +); + +// The actual filter that we're editing if it is a _saved_ filter or prefill; +// void otherwise. +var gFilter; +// cache the key elements we need +var gFilterList; +// The filter name as it appears in the "Filter Name" field of dialog. +var gFilterNameElement; +var gFilterTypeSelector; +var gFilterBundle; +var gPreFillName; +var gFilterActionList; +var gCustomActions = null; +var gFilterType; +var gFilterPosition = 0; + +var gFilterActionStrings = [ + "none", + "movemessage", + "setpriorityto", + "deletemessage", + "markasread", + "ignorethread", + "watchthread", + "markasflagged", + "label", + "replytomessage", + "forwardmessage", + "stopexecution", + "deletefrompopserver", + "leaveonpopserver", + "setjunkscore", + "fetchfrompopserver", + "copymessage", + "addtagtomessage", + "ignoresubthread", + "markasunread", +]; + +// A temporary filter with the current state of actions in the UI. +var gTempFilter = null; +// nsIMsgRuleAction[] - the currently defined actions in the order they will be run. +var gActionListOrdered = null; + +var gFilterEditorMsgWindow = null; + +window.addEventListener("DOMContentLoaded", filterEditorOnLoad, { once: true }); +document.addEventListener("dialogaccept", onAccept); + +function filterEditorOnLoad() { + getCustomActions(); + initializeSearchWidgets(); + initializeFilterWidgets(); + + gFilterBundle = document.getElementById("bundle_filter"); + + if ("arguments" in window && window.arguments[0]) { + var args = window.arguments[0]; + + if ("filterList" in args) { + gFilterList = args.filterList; + // the postPlugin filters cannot be applied to servers that are + // deferred, (you must define them on the deferredTo server instead). + let server = gFilterList.folder.server; + if (server.rootFolder != server.rootMsgFolder) { + gFilterTypeSelector.disableDeferredAccount(); + } + } + + if ("filterPosition" in args) { + gFilterPosition = args.filterPosition; + } + + if ("filter" in args) { + // editing a filter + gFilter = window.arguments[0].filter; + initializeDialog(gFilter); + } else { + if (gFilterList) { + setSearchScope(getScopeFromFilterList(gFilterList)); + } + // if doing prefill filter create a new filter and populate it. + if ("filterName" in args) { + gPreFillName = args.filterName; + + // Passing null as the parameter to createFilter to keep the name empty + // until later where we assign the name. + gFilter = gFilterList.createFilter(null); + + var term = gFilter.createTerm(); + + term.attrib = Ci.nsMsgSearchAttrib.Default; + if ("fieldName" in args && args.fieldName) { + // fieldName should contain the name of the field in which to search, + // from nsMsgSearchTerm.cpp::SearchAttribEntryTable, e.g. "to" or "cc" + try { + term.attrib = term.getAttributeFromString(args.fieldName); + } catch (e) { + /* Invalid string is fine, just ignore it. */ + } + } + if (term.attrib == Ci.nsMsgSearchAttrib.Default) { + term.attrib = Ci.nsMsgSearchAttrib.Sender; + } + + term.op = Ci.nsMsgSearchOp.Is; + term.booleanAnd = gSearchBooleanRadiogroup.value == "and"; + + var termValue = term.value; + termValue.attrib = term.attrib; + termValue.str = gPreFillName; + + term.value = termValue; + + gFilter.appendTerm(term); + + // the default action for news filters is Delete + // for everything else, it's MoveToFolder + var filterAction = gFilter.createAction(); + filterAction.type = + getScopeFromFilterList(gFilterList) == Ci.nsMsgSearchScope.newsFilter + ? Ci.nsMsgFilterAction.Delete + : Ci.nsMsgFilterAction.MoveToFolder; + gFilter.appendAction(filterAction); + initializeDialog(gFilter); + } else if ("copiedFilter" in args) { + // we are copying a filter + let copiedFilter = args.copiedFilter; + let copiedName = gFilterBundle.getFormattedString( + "copyToNewFilterName", + [copiedFilter.filterName] + ); + let newFilter = gFilterList.createFilter(copiedName); + + // copy the actions + for (let i = 0; i < copiedFilter.actionCount; i++) { + let filterAction = copiedFilter.getActionAt(i); + newFilter.appendAction(filterAction); + } + + // copy the search terms + for (let searchTerm of copiedFilter.searchTerms) { + let newTerm = newFilter.createTerm(); + newTerm.attrib = searchTerm.attrib; + newTerm.op = searchTerm.op; + newTerm.booleanAnd = searchTerm.booleanAnd; + newTerm.value = searchTerm.value; + newFilter.appendTerm(newTerm); + } + + newFilter.filterType = copiedFilter.filterType; + + gPreFillName = copiedName; + gFilter = newFilter; + + initializeDialog(gFilter); + + // We reset the filter name, because otherwise the saveFilter() + // function thinks we are editing a filter, and will thus skip the name + // uniqueness check. + gFilter.filterName = ""; + } else { + // fake the first more button press + onMore(null); + } + } + } + + if (!gFilter) { + // This is a new filter. Set to both Incoming and Manual contexts. + gFilterTypeSelector.setType( + Ci.nsMsgFilterType.Incoming | Ci.nsMsgFilterType.Manual + ); + } + + // in the case of a new filter, we may not have an action row yet. + ensureActionRow(); + gFilterType = gFilterTypeSelector.getType(); + + gFilterNameElement.select(); + // This call is required on mac and linux. It has no effect under win32. See bug 94800. + gFilterNameElement.focus(); +} + +function onEnterInSearchTerm(event) { + if (event.ctrlKey || (Services.appinfo.OS == "Darwin" && event.metaKey)) { + // If accel key (Ctrl on Win/Linux, Cmd on Mac) was held too, accept the dialog. + document.querySelector("dialog").acceptDialog(); + } else { + // If only plain Enter was pressed, add a new rule line. + onMore(event); + } +} + +function onAccept(event) { + try { + if (!saveFilter()) { + event.preventDefault(); + return; + } + } catch (e) { + console.error(e); + event.preventDefault(); + return; + } + + // parent should refresh filter list.. + // this should REALLY only happen when some criteria changes that + // are displayed in the filter dialog, like the filter name + window.arguments[0].refresh = true; + window.arguments[0].newFilter = gFilter; +} + +function duplicateFilterNameExists(filterName) { + if (gFilterList) { + for (var i = 0; i < gFilterList.filterCount; i++) { + if (filterName == gFilterList.getFilterAt(i).filterName) { + return true; + } + } + } + return false; +} + +function getScopeFromFilterList(filterList) { + if (!filterList) { + dump("yikes, null filterList\n"); + return Ci.nsMsgSearchScope.offlineMail; + } + return filterList.folder.server.filterScope; +} + +function getScope(filter) { + return getScopeFromFilterList(filter.filterList); +} + +function initializeFilterWidgets() { + gFilterNameElement = document.getElementById("filterName"); + gFilterActionList = document.getElementById("filterActionList"); + initializeFilterTypeSelector(); +} + +function initializeFilterTypeSelector() { + /** + * This object controls code interaction with the widget allowing specifying + * the filter type (event when the filter is run). + */ + gFilterTypeSelector = { + checkBoxManual: document.getElementById("runManual"), + checkBoxIncoming: document.getElementById("runIncoming"), + + menulistIncoming: document.getElementById("pluginsRunOrder"), + + menuitemBeforePlugins: document.getElementById("runBeforePlugins"), + menuitemAfterPlugins: document.getElementById("runAfterPlugins"), + + checkBoxArchive: document.getElementById("runArchive"), + checkBoxOutgoing: document.getElementById("runOutgoing"), + checkBoxPeriodic: document.getElementById("runPeriodic"), + + /** + * Returns the currently set filter type (checkboxes) in terms + * of a Ci.Ci.nsMsgFilterType value. + */ + getType() { + let type = Ci.nsMsgFilterType.None; + + if (this.checkBoxManual.checked) { + type |= Ci.nsMsgFilterType.Manual; + } + + if (this.checkBoxIncoming.checked) { + if (this.menulistIncoming.selectedItem == this.menuitemAfterPlugins) { + type |= Ci.nsMsgFilterType.PostPlugin; + } else if ( + getScopeFromFilterList(gFilterList) == Ci.nsMsgSearchScope.newsFilter + ) { + type |= Ci.nsMsgFilterType.NewsRule; + } else { + type |= Ci.nsMsgFilterType.InboxRule; + } + } + + if (this.checkBoxArchive.checked) { + type |= Ci.nsMsgFilterType.Archive; + } + + if (this.checkBoxOutgoing.checked) { + type |= Ci.nsMsgFilterType.PostOutgoing; + } + + if (this.checkBoxPeriodic.checked) { + type |= Ci.nsMsgFilterType.Periodic; + } + + return type; + }, + + /** + * Sets the checkboxes to represent the filter type passed in. + * + * @param aType the filter type to set in terms + * of Ci.Ci.nsMsgFilterType values. + */ + setType(aType) { + // If there is no type (event) requested, force "when manually run" + if (aType == Ci.nsMsgFilterType.None) { + aType = Ci.nsMsgFilterType.Manual; + } + + this.checkBoxManual.checked = aType & Ci.nsMsgFilterType.Manual; + + this.checkBoxIncoming.checked = + aType & (Ci.nsMsgFilterType.PostPlugin | Ci.nsMsgFilterType.Incoming); + + this.menulistIncoming.selectedItem = + aType & Ci.nsMsgFilterType.PostPlugin + ? this.menuitemAfterPlugins + : this.menuitemBeforePlugins; + + this.checkBoxArchive.checked = aType & Ci.nsMsgFilterType.Archive; + + this.checkBoxOutgoing.checked = aType & Ci.nsMsgFilterType.PostOutgoing; + + this.checkBoxPeriodic.checked = aType & Ci.nsMsgFilterType.Periodic; + const periodMinutes = gFilterList.folder.server.getIntValue( + "periodicFilterRateMinutes" + ); + document.getElementById("runPeriodic").label = PluralForm.get( + periodMinutes, + gFilterBundle.getString("contextPeriodic.label") + ).replace("#1", periodMinutes); + + this.updateClassificationMenu(); + }, + + /** + * Enable the "before/after classification" menulist depending on + * whether "run when incoming mail" is selected. + */ + updateClassificationMenu() { + this.menulistIncoming.disabled = !this.checkBoxIncoming.checked; + updateFilterType(); + }, + + /** + * Disable the options unsuitable for deferred accounts. + */ + disableDeferredAccount() { + this.menuitemAfterPlugins.disabled = true; + this.checkBoxOutgoing.disabled = true; + }, + }; +} + +function initializeDialog(filter) { + gFilterNameElement.value = filter.filterName; + gFilterTypeSelector.setType(filter.filterType); + + let numActions = filter.actionCount; + for (let actionIndex = 0; actionIndex < numActions; actionIndex++) { + let filterAction = filter.getActionAt(actionIndex); + + let newActionRow = document.createXULElement("richlistitem", { + is: "ruleaction-richlistitem", + }); + newActionRow.setAttribute("initialActionIndex", actionIndex); + newActionRow.className = "ruleaction"; + gFilterActionList.appendChild(newActionRow); + newActionRow.setAttribute( + "value", + filterAction.type == Ci.nsMsgFilterAction.Custom + ? filterAction.customId + : gFilterActionStrings[filterAction.type] + ); + newActionRow.setAttribute("onfocus", "this.storeFocus();"); + } + + var gSearchScope = getFilterScope( + getScope(filter), + filter.filterType, + filter.filterList + ); + initializeSearchRows(gSearchScope, filter.searchTerms); + setFilterScope(filter.filterType, filter.filterList); +} + +function ensureActionRow() { + // make sure we have at least one action row visible to the user + if (!gFilterActionList.getRowCount()) { + let newActionRow = document.createXULElement("richlistitem", { + is: "ruleaction-richlistitem", + }); + newActionRow.className = "ruleaction"; + gFilterActionList.appendChild(newActionRow); + newActionRow.mRemoveButton.disabled = true; + } +} + +// move to overlay +function saveFilter() { + // See if at least one filter type (activation event) is selected. + if (gFilterType == Ci.nsMsgFilterType.None) { + Services.prompt.alert( + window, + gFilterBundle.getString("mustHaveFilterTypeTitle"), + gFilterBundle.getString("mustHaveFilterTypeMessage") + ); + return false; + } + + let filterName = gFilterNameElement.value; + // If we think have a duplicate, then we need to check that if we + // have an original filter name (i.e. we are editing a filter), then + // we must check that the original is not the current as that is what + // the duplicateFilterNameExists function will have picked up. + if ( + (!gFilter || gFilter.filterName != filterName) && + duplicateFilterNameExists(filterName) + ) { + Services.prompt.alert( + window, + gFilterBundle.getString("cannotHaveDuplicateFilterTitle"), + gFilterBundle.getString("cannotHaveDuplicateFilterMessage") + ); + return false; + } + + // Check that all of the search attributes and operators are valid. + function rule_desc(index, obj) { + return ( + index + + 1 + + " (" + + obj.searchattribute.label + + ", " + + obj.searchoperator.label + + ")" + ); + } + + let invalidRule = false; + for (let index = 0; index < gSearchTerms.length; index++) { + let obj = gSearchTerms[index].obj; + // We don't need to check validity of matchAll terms + if (obj.matchAll) { + continue; + } + + // the term might be an offscreen one that we haven't initialized yet + let searchTerm = obj.searchTerm; + if (!searchTerm && !gSearchTerms[index].initialized) { + continue; + } + + if (isNaN(obj.searchattribute.value)) { + // is this a custom term? + let customTerm = MailServices.filters.getCustomTerm( + obj.searchattribute.value + ); + if (!customTerm) { + invalidRule = true; + console.error( + "Filter not saved because custom search term '" + + obj.searchattribute.value + + "' in rule " + + rule_desc(index, obj) + + " not found" + ); + } else if ( + !customTerm.getAvailable(obj.searchScope, obj.searchattribute.value) + ) { + invalidRule = true; + console.error( + "Filter not saved because custom search term '" + + customTerm.name + + "' in rule " + + rule_desc(index, obj) + + " not available" + ); + } + } else { + let otherHeader = Ci.nsMsgSearchAttrib.OtherHeader; + let attribValue = + obj.searchattribute.value > otherHeader + ? otherHeader + : obj.searchattribute.value; + if ( + !obj.searchattribute.validityTable.getAvailable( + attribValue, + obj.searchoperator.value + ) + ) { + invalidRule = true; + console.error( + "Filter not saved because standard search term '" + + attribValue + + "' in rule " + + rule_desc(index, obj) + + " not available in this context" + ); + } + } + + if (invalidRule) { + Services.prompt.alert( + window, + gFilterBundle.getString("searchTermsInvalidTitle"), + gFilterBundle.getFormattedString("searchTermsInvalidRule", [ + obj.searchattribute.label, + obj.searchoperator.label, + ]) + ); + return false; + } + } + + // before we go any further, validate each specified filter action, abort the save + // if any of the actions is invalid... + for (let index = 0; index < gFilterActionList.itemCount; index++) { + var listItem = gFilterActionList.getItemAtIndex(index); + if (!listItem.validateAction()) { + return false; + } + } + + // if we made it here, all of the actions are valid, so go ahead and save the filter + let isNewFilter; + if (!gFilter) { + // This is a new filter + gFilter = gFilterList.createFilter(filterName); + isNewFilter = true; + gFilter.enabled = true; + } else { + // We are working with an existing filter object, + // either editing or using prefill + gFilter.filterName = filterName; + // Prefilter is treated as a new filter. + if (gPreFillName) { + isNewFilter = true; + gFilter.enabled = true; + } else { + isNewFilter = false; + } + + gFilter.clearActionList(); + } + + // add each filteraction to the filter + for (let index = 0; index < gFilterActionList.itemCount; index++) { + gFilterActionList.getItemAtIndex(index).saveToFilter(gFilter); + } + + // If we do not have a filter name at this point, generate one. + if (!gFilter.filterName) { + AssignMeaningfulName(); + } + + gFilter.filterType = gFilterType; + gFilter.searchTerms = saveSearchTerms(gFilter.searchTerms, gFilter); + + if (isNewFilter) { + // new filter - insert into gFilterList + gFilterList.insertFilterAt(gFilterPosition, gFilter); + } + + // success! + return true; +} + +/** + * Check if the list of actions the user created will be executed in a different order. + * Exposes a note to the user if that is the case. + */ +function checkActionsReorder() { + setTimeout(_checkActionsReorder, 0); +} + +/** + * This should be called from setTimeout otherwise some of the elements calling + * may not be fully initialized yet (e.g. we get ".saveToFilter is not a function"). + * It is OK to schedule multiple timeouts with this function. + */ +function _checkActionsReorder() { + // Create a temporary disposable filter and add current actions to it. + if (!gTempFilter) { + gTempFilter = gFilterList.createFilter(""); + } else { + gTempFilter.clearActionList(); + } + + for (let index = 0; index < gFilterActionList.itemCount; index++) { + gFilterActionList.getItemAtIndex(index).saveToFilter(gTempFilter); + } + + // Now get the actions out of the filter in the order they will be executed in. + gActionListOrdered = gTempFilter.sortedActionList; + + // Compare the two lists. + let statusBar = document.getElementById("statusbar"); + for (let index = 0; index < gActionListOrdered.length; index++) { + if (index != gTempFilter.getActionIndex(gActionListOrdered[index])) { + // If the lists are not the same unhide the status bar and show warning. + statusBar.style.visibility = "visible"; + return; + } + } + + statusBar.style.visibility = "hidden"; +} + +/** + * Show a dialog with the ordered list of actions. + * The fetching of action label and argument is separated from checkActionsReorder + * function to make that one more lightweight. The list is built only upon + * user request. + */ +function showActionsOrder() { + // Fetch the actions and arguments as a string. + let actionStrings = []; + for (let i = 0; i < gFilterActionList.itemCount; i++) { + let ruleAction = gFilterActionList.getItemAtIndex(i); + let actionTarget = ruleAction.children[1]; + let actionItem = actionTarget.ruleactiontargetElement; + let actionItemLabel = actionItem && actionItem.children[0].label; + + let actionString = { + label: ruleAction.mRuleActionType.label, + argument: "", + }; + if (actionItem) { + if (actionItemLabel) { + actionString.argument = actionItemLabel; + } else { + actionString.argument = actionItem.children[0].value; + } + } + actionStrings.push(actionString); + } + + // Present a nicely formatted list of action names and arguments. + let actionList = gFilterBundle.getString("filterActionOrderExplanation"); + for (let i = 0; i < gActionListOrdered.length; i++) { + let actionIndex = gTempFilter.getActionIndex(gActionListOrdered[i]); + let action = actionStrings[actionIndex]; + actionList += gFilterBundle.getFormattedString("filterActionItem", [ + i + 1, + action.label, + action.argument, + ]); + } + + Services.prompt.confirmEx( + window, + gFilterBundle.getString("filterActionOrderTitle"), + actionList, + Services.prompt.BUTTON_TITLE_OK, + null, + null, + null, + null, + { value: false } + ); +} + +function AssignMeaningfulName() { + // termRoot points to the first search object, which is the one we care about. + let termRoot = gSearchTerms[0].obj; + // stub is used as the base name for a filter. + let stub; + + // If this is a Match All Messages Filter, we already know the name to assign. + if (termRoot.matchAll) { + stub = gFilterBundle.getString("matchAllFilterName"); + } else { + // Assign a name based on the first search term. + let term = termRoot.searchattribute.label; + let operator = termRoot.searchoperator.label; + let value = termRoot.searchvalue.getReadableValue(); + stub = gFilterBundle.getFormattedString("filterAutoNameStr", [ + term, + operator, + value, + ]); + } + + // Whatever name we have used, 'uniquify' it. + let tempName = stub; + let count = 1; + while (duplicateFilterNameExists(tempName)) { + count++; + tempName = `${stub} ${count}`; + } + gFilter.filterName = tempName; +} + +function UpdateAfterCustomHeaderChange() { + updateSearchAttributes(); +} + +// if you use msgWindow, please make sure that destructor gets called when you close the "window" +function GetFilterEditorMsgWindow() { + if (!gFilterEditorMsgWindow) { + var msgWindowContractID = "@mozilla.org/messenger/msgwindow;1"; + var nsIMsgWindow = Ci.nsIMsgWindow; + gFilterEditorMsgWindow = + Cc[msgWindowContractID].createInstance(nsIMsgWindow); + gFilterEditorMsgWindow.domWindow = window; + gFilterEditorMsgWindow.rootDocShell.appType = Ci.nsIDocShell.APP_TYPE_MAIL; + } + return gFilterEditorMsgWindow; +} + +function SetBusyCursor(window, enable) { + // setCursor() is only available for chrome windows. + // However one of our frames is the start page which + // is a non-chrome window, so check if this window has a + // setCursor method + if ("setCursor" in window) { + if (enable) { + window.setCursor("wait"); + } else { + window.setCursor("auto"); + } + } +} + +/* globals openHelp */ +// suite/components/helpviewer/content/contextHelp.js +function doHelpButton() { + openHelp("mail-filters"); +} + +function getCustomActions() { + if (!gCustomActions) { + gCustomActions = MailServices.filters.getCustomActions(); + } +} + +function updateFilterType() { + gFilterType = gFilterTypeSelector.getType(); + setFilterScope(gFilterType, gFilterList); + + // set valid actions + var ruleActions = gFilterActionList.getElementsByAttribute( + "class", + "ruleaction" + ); + for (var i = 0; i < ruleActions.length; i++) { + ruleActions[i].mRuleActionType.hideInvalidActions(); + } +} + +// Given a filter type, set the global search scope to the filter scope +function setFilterScope(aFilterType, aFilterList) { + let filterScope = getFilterScope( + getScopeFromFilterList(aFilterList), + aFilterType, + aFilterList + ); + setSearchScope(filterScope); +} + +// +// Given the base filter scope for a server, and the filter +// type, return the scope used for filter. This assumes a +// hierarchy of contexts, with incoming the most restrictive, +// followed by manual and post-plugin. +function getFilterScope(aServerFilterScope, aFilterType, aFilterList) { + if (aFilterType & Ci.nsMsgFilterType.Incoming) { + return aServerFilterScope; + } + + // Manual or PostPlugin + // local mail allows body and junk types + if (aServerFilterScope == Ci.nsMsgSearchScope.offlineMailFilter) { + return Ci.nsMsgSearchScope.offlineMail; + } + // IMAP and NEWS online don't allow body + return Ci.nsMsgSearchScope.onlineManual; +} + +/** + * Re-focus the action that was focused before focus was lost. + */ +function setLastActionFocus() { + let lastAction = gFilterActionList.getAttribute("focusedAction"); + if (!lastAction || lastAction < 0) { + lastAction = 0; + } + if (lastAction >= gFilterActionList.itemCount) { + lastAction = gFilterActionList.itemCount - 1; + } + + gFilterActionList.getItemAtIndex(lastAction).mRuleActionType.focus(); +} |