/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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/. */ Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource:///modules/mailServices.js"); Components.utils.import("resource:///modules/MailUtils.js"); // 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 gSessionFolderListenerAdded = false; 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; // A nsIArray of the currently defined actions in the order they will be run. var gActionListOrdered = null; var gFilterEditorMsgWindow = null; var nsMsgFilterAction = Components.interfaces.nsMsgFilterAction; var nsMsgFilterType = Components.interfaces.nsMsgFilterType; var nsIMsgRuleAction = Components.interfaces.nsIMsgRuleAction; var nsMsgSearchScope = Components.interfaces.nsMsgSearchScope; 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 = Components.interfaces.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 == Components.interfaces.nsMsgSearchAttrib.Default) term.attrib = Components.interfaces.nsMsgSearchAttrib.Sender; term.op = Components.interfaces.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) == nsMsgSearchScope.newsFilter) ? nsMsgFilterAction.Delete : nsMsgFilterAction.MoveToFolder; gFilter.appendAction(filterAction); initializeDialog(gFilter); } else if ("copiedFilter" in args) { // we are copying a filter var copiedFilter = args.copiedFilter; var 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 i = 0; i < copiedFilter.searchTerms.Count(); i++) { var searchTerm = copiedFilter.searchTerms.QueryElementAt(i, Components.interfaces.nsIMsgSearchTerm); var newTerm = newFilter.createTerm(); newTerm.attrib = searchTerm.attrib; newTerm.op = searchTerm.op; newTerm.booleanAnd = searchTerm.booleanAnd; newTerm.value = searchTerm.value; newFilter.appendTerm(newTerm); }; 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(nsMsgFilterType.Incoming | 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 filterEditorOnUnload() { if (gSessionFolderListenerAdded) MailServices.mailSession.RemoveFolderListener(gFolderListener); } 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.getElementById("FilterEditor").acceptDialog(); } else { // If only plain Enter was pressed, add a new rule line. onMore(event); } } function onAccept() { try { if (!saveFilter()) return false; } catch(e) {Components.utils.reportError(e); return false;} // 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; return true; } // the folderListener object var gFolderListener = { OnItemAdded: function(parentItem, item) {}, OnItemRemoved: function(parentItem, item){}, OnItemPropertyChanged: function(item, property, oldValue, newValue) {}, OnItemIntPropertyChanged: function(item, property, oldValue, newValue) {}, OnItemBoolPropertyChanged: function(item, property, oldValue, newValue) {}, OnItemUnicharPropertyChanged: function(item, property, oldValue, newValue){}, OnItemPropertyFlagChanged: function(item, property, oldFlag, newFlag) {}, OnItemEvent: function(folder, event) { var eventType = event.toString(); if (eventType == "FolderCreateCompleted") { gActionTargetElement.selectFolder(folder); SetBusyCursor(window, false); } else if (eventType == "FolderCreateFailed") SetBusyCursor(window, false); } } 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 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"), /** * Returns the currently set filter type (checkboxes) in terms * of a Components.interfaces.nsMsgFilterType value. */ getType: function() { let type = nsMsgFilterType.None; if (this.checkBoxManual.checked) type |= nsMsgFilterType.Manual; if (this.checkBoxIncoming.checked) { if (this.menulistIncoming.selectedItem == this.menuitemAfterPlugins) { type |= nsMsgFilterType.PostPlugin; } else { // this.menuitemBeforePlugins selected if (getScopeFromFilterList(gFilterList) == nsMsgSearchScope.newsFilter) type |= nsMsgFilterType.NewsRule; else type |= nsMsgFilterType.InboxRule; } } if (this.checkBoxArchive.checked) type |= nsMsgFilterType.Archive; if (this.checkBoxOutgoing.checked) type |= nsMsgFilterType.PostOutgoing; return type; }, /** * Sets the checkboxes to represent the filter type passed in. * * @param aType the filter type to set in terms * of Components.interfaces.nsMsgFilterType values. */ setType: function(aType) { // If there is no type (event) requested, force "when manually run" if (aType == nsMsgFilterType.None) aType = nsMsgFilterType.Manual; this.checkBoxManual.checked = aType & nsMsgFilterType.Manual; this.checkBoxIncoming.checked = aType & (nsMsgFilterType.PostPlugin | nsMsgFilterType.Incoming); this.menulistIncoming.selectedItem = aType & nsMsgFilterType.PostPlugin ? this.menuitemAfterPlugins : this.menuitemBeforePlugins; this.checkBoxArchive.checked = aType & nsMsgFilterType.Archive; this.checkBoxOutgoing.checked = aType & nsMsgFilterType.PostOutgoing; this.updateClassificationMenu(); }, /** * Enable the "before/after classification" menulist depending on * whether "run when incoming mail" is selected. */ updateClassificationMenu: function() { this.menulistIncoming.disabled = !this.checkBoxIncoming.checked; updateFilterType(); }, /** * Disable the options unsuitable for deferred accounts. */ disableDeferredAccount: function() { this.menuitemAfterPlugins.disabled = true; this.checkBoxOutgoing.disabled = true; } }; } function initializeDialog(filter) { gFilterNameElement.value = filter.filterName; let filterType = filter.filterType; gFilterTypeSelector.setType(filter.filterType); let numActions = filter.actionCount; for (let actionIndex = 0; actionIndex < numActions; actionIndex++) { let filterAction = filter.getActionAt(actionIndex); var newActionRow = document.createElement('listitem'); newActionRow.setAttribute('initialActionIndex', actionIndex); newActionRow.className = 'ruleaction'; gFilterActionList.appendChild(newActionRow); newActionRow.setAttribute('value', filterAction.type == 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()) { var newActionRow = document.createElement('listitem'); 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 == 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; Components.utils.reportError("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; Components.utils.reportError("Filter not saved because custom search term '" + customTerm.name + "' in rule " + rule_desc(index, obj) + " not available"); } } } else { let otherHeader = Components.interfaces.nsMsgSearchAttrib.OtherHeader; let attribValue = (obj.searchattribute.value > otherHeader) ? otherHeader : obj.searchattribute.value; if (!obj.searchattribute .validityTable .getAvailable(attribValue, obj.searchoperator.value)) { invalidRule = true; Components.utils.reportError("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; 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.queryElementAt(index, nsIMsgRuleAction))) { // 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 index = 0; index < gFilterActionList.itemCount; index++) gFilterActionList.getItemAtIndex(index).getActionStrings(actionStrings); // 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.queryElementAt(i, nsIMsgRuleAction)); 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 searchValue = termRoot.searchvalue; let selIndex = searchValue.getAttribute( "selectedIndex" ); let children = document.getAnonymousNodes(searchValue); let activeItem = children[selIndex]; let attribs = Components.interfaces.nsMsgSearchAttrib; // Term, Operator and Value are the three parts of a filter match // Term and Operator are easy to retrieve let term = termRoot.searchattribute.label; let operator = termRoot.searchoperator.label; // Values are either popup menu items or edit fields. // For popup menus use activeItem.label; for // edit fields, activeItem.value let value; switch (Number(termRoot.searchattribute.value)) { case attribs.Priority: case attribs.MsgStatus: case attribs.Keywords: case attribs.HasAttachmentStatus: case attribs.JunkStatus: case attribs.JunkScoreOrigin: if (activeItem) value = activeItem.label; else value = ""; break; default: try { value = activeItem.value; } catch (ex) { // We should never get here, but for safety's sake, // let's name the filter "Untitled Filter". stub = gFilterBundle.getString( "untitledFilterName" ); // Do not 'Return'. Instead fall through and deal with the untitled filter below. } break; } // We are now ready to name the filter. // If at this point stub is empty, we know that this is not a Match All Filter // and is not an "untitledFilterName" Filter, so assign it a name using // a string format from the Filter Bundle. if (!stub) 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 GetFirstSelectedMsgFolder() { var selectedFolder = gActionTargetElement.getAttribute("uri"); if (!selectedFolder) return null; var msgFolder = MailUtils.getFolderForURI(selectedFolder, true); return msgFolder; } function SearchNewFolderOkCallback(name, uri) { var msgFolder = MailUtils.getFolderForURI(uri, true); var imapFolder = null; try { imapFolder = msgFolder.QueryInterface(Components.interfaces.nsIMsgImapMailFolder); } catch(ex) {} if (imapFolder) //imapFolder creation is asynchronous. { if (!gSessionFolderListenerAdded) { try { let notifyFlags = Components.interfaces.nsIFolderListener.event; MailServices.mailSession.AddFolderListener(gFolderListener, notifyFlags); gSessionFolderListenerAdded = true; } catch (ex) { Components.utils.reportError("Error adding to session: " + ex + "\n"); } } } var msgWindow = GetFilterEditorMsgWindow(); if (imapFolder) SetBusyCursor(window, true); msgFolder.createSubfolder(name, msgWindow); if (!imapFolder) { var curFolder = uri+"/"+encodeURIComponent(name); let folder = MailUtils.getFolderForURI(curFolder); gActionTargetElement.selectFolder(folder); } } 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 = Components.interfaces.nsIMsgWindow; gFilterEditorMsgWindow = Components.classes[msgWindowContractID].createInstance(nsIMsgWindow); gFilterEditorMsgWindow.domWindow = window; gFilterEditorMsgWindow.rootDocShell.appType = Components.interfaces.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"); } } function doHelpButton() { openHelp("mail-filters"); } function getCustomActions() { if (!gCustomActions) { gCustomActions = []; let customActionsEnum = MailServices.filters.getCustomActions(); while (customActionsEnum.hasMoreElements()) gCustomActions.push(customActionsEnum.getNext().QueryInterface( Components.interfaces.nsIMsgFilterCustomAction)); } } 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 & nsMsgFilterType.Incoming) return aServerFilterScope; // Manual or PostPlugin // local mail allows body and junk types if (aServerFilterScope == nsMsgSearchScope.offlineMailFilter) return nsMsgSearchScope.offlineMail; // IMAP and NEWS online don't allow body return 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.menulist.focus(); }