summaryrefslogtreecommitdiffstats
path: root/mailnews/base/search/content
diff options
context:
space:
mode:
Diffstat (limited to 'mailnews/base/search/content')
-rw-r--r--mailnews/base/search/content/CustomHeaders.js203
-rw-r--r--mailnews/base/search/content/CustomHeaders.xul57
-rw-r--r--mailnews/base/search/content/FilterEditor.js854
-rw-r--r--mailnews/base/search/content/FilterEditor.xul125
-rw-r--r--mailnews/base/search/content/searchTermOverlay.js536
-rw-r--r--mailnews/base/search/content/searchTermOverlay.xul66
-rw-r--r--mailnews/base/search/content/searchWidgets.xml738
-rw-r--r--mailnews/base/search/content/viewLog.js36
-rw-r--r--mailnews/base/search/content/viewLog.xul49
9 files changed, 2664 insertions, 0 deletions
diff --git a/mailnews/base/search/content/CustomHeaders.js b/mailnews/base/search/content/CustomHeaders.js
new file mode 100644
index 000000000..174ba100e
--- /dev/null
+++ b/mailnews/base/search/content/CustomHeaders.js
@@ -0,0 +1,203 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+ * 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");
+
+var gAddButton;
+var gRemoveButton;
+var gHeaderInputElement;
+var gArrayHdrs;
+var gHdrsList;
+var gContainer;
+var gFilterBundle=null;
+var gCustomBundle=null;
+
+function onLoad()
+{
+ let hdrs = Services.prefs.getCharPref("mailnews.customHeaders");
+ gHeaderInputElement = document.getElementById("headerInput");
+ gHeaderInputElement.focus();
+
+ gHdrsList = document.getElementById("headerList");
+ gArrayHdrs = new Array();
+ gAddButton = document.getElementById("addButton");
+ gRemoveButton = document.getElementById("removeButton");
+
+ initializeDialog(hdrs);
+ updateAddButton(true);
+ updateRemoveButton();
+}
+
+function initializeDialog(hdrs)
+{
+ if (hdrs)
+ {
+ hdrs = hdrs.replace(/\s+/g,''); //remove white spaces before splitting
+ gArrayHdrs = hdrs.split(":");
+ for (var i = 0; i < gArrayHdrs.length; i++)
+ if (!gArrayHdrs[i])
+ gArrayHdrs.splice(i,1); //remove any null elements
+ initializeRows();
+ }
+}
+
+function initializeRows()
+{
+ for (var i = 0; i < gArrayHdrs.length; i++)
+ addRow(TrimString(gArrayHdrs[i]));
+}
+
+function onTextInput()
+{
+ // enable the add button if the user has started to type text
+ updateAddButton( (gHeaderInputElement.value == "") );
+}
+
+function onOk()
+{
+ if (gArrayHdrs.length)
+ {
+ var hdrs;
+ if (gArrayHdrs.length == 1)
+ hdrs = gArrayHdrs;
+ else
+ hdrs = gArrayHdrs.join(": ");
+ Services.prefs.setCharPref("mailnews.customHeaders", hdrs);
+ // flush prefs to disk, in case we crash, to avoid dataloss and problems with filters that use the custom headers
+ Services.prefs.savePrefFile(null);
+ }
+ else
+ {
+ Services.prefs.clearUserPref("mailnews.customHeaders"); //clear the pref, no custom headers
+ }
+
+ window.arguments[0].selectedVal = gHdrsList.selectedItem ? gHdrsList.selectedItem.label : null;
+ return true;
+}
+
+function customHeaderOverflow()
+{
+ var nsMsgSearchAttrib = Components.interfaces.nsMsgSearchAttrib;
+ if (gArrayHdrs.length >= (nsMsgSearchAttrib.kNumMsgSearchAttributes - nsMsgSearchAttrib.OtherHeader - 1))
+ {
+ if (!gFilterBundle)
+ gFilterBundle = document.getElementById("bundle_filter");
+
+ var alertText = gFilterBundle.getString("customHeaderOverflow");
+ Services.prompt.alert(window, null, alertText);
+ return true;
+ }
+ return false;
+}
+
+function onAddHeader()
+{
+ var newHdr = TrimString(gHeaderInputElement.value);
+
+ if (!isRFC2822Header(newHdr)) // if user entered an invalid rfc822 header field name, bail out.
+ {
+ if (!gCustomBundle)
+ gCustomBundle = document.getElementById("bundle_custom");
+
+ var alertText = gCustomBundle.getString("colonInHeaderName");
+ Services.prompt.alert(window, null, alertText);
+ return;
+ }
+
+ gHeaderInputElement.value = "";
+ if (!newHdr || customHeaderOverflow())
+ return;
+ if (!duplicateHdrExists(newHdr))
+ {
+ gArrayHdrs[gArrayHdrs.length] = newHdr;
+ var newItem = addRow(newHdr);
+ gHdrsList.selectItem (newItem); // make sure the new entry is selected in the tree
+ // now disable the add button
+ updateAddButton(true);
+ gHeaderInputElement.focus(); // refocus the input field for the next custom header
+ }
+}
+
+function isRFC2822Header(hdr)
+{
+ var charCode;
+ for (var i = 0; i < hdr.length; i++)
+ {
+ charCode = hdr.charCodeAt(i);
+ //58 is for colon and 33 and 126 are us-ascii bounds that should be used for header field name, as per rfc2822
+
+ if (charCode < 33 || charCode == 58 || charCode > 126)
+ return false;
+ }
+ return true;
+}
+
+function duplicateHdrExists(hdr)
+{
+ for (var i = 0;i < gArrayHdrs.length; i++)
+ {
+ if (gArrayHdrs[i] == hdr)
+ return true;
+ }
+ return false;
+}
+
+function onRemoveHeader()
+{
+ var listitem = gHdrsList.selectedItems[0]
+ if (!listitem) return;
+ listitem.remove();
+ var selectedHdr = GetListItemAttributeStr(listitem);
+ var j=0;
+ for (var i = 0; i < gArrayHdrs.length; i++)
+ {
+ if (gArrayHdrs[i] == selectedHdr)
+ {
+ gArrayHdrs.splice(i,1);
+ break;
+ }
+ }
+}
+
+function GetListItemAttributeStr(listitem)
+{
+ if (listitem)
+ return TrimString(listitem.getAttribute("label"));
+
+ return "";
+}
+
+function addRow(newHdr)
+{
+ var listitem = document.createElement("listitem");
+ listitem.setAttribute("label", newHdr);
+ gHdrsList.appendChild(listitem);
+ return listitem;
+}
+
+function updateAddButton(aDisable)
+{
+ // only update the button if the disabled state changed
+ if (aDisable == gAddButton.disabled)
+ return;
+
+ gAddButton.disabled = aDisable;
+ document.documentElement.defaultButton = aDisable ? "accept" : "extra1";
+}
+
+function updateRemoveButton()
+{
+ var headerSelected = (gHdrsList.selectedItems.length > 0);
+ gRemoveButton.disabled = !headerSelected;
+ if (gRemoveButton.disabled)
+ gHeaderInputElement.focus();
+}
+
+//Remove whitespace from both ends of a string
+function TrimString(string)
+{
+ if (!string) return "";
+ return string.trim();
+}
diff --git a/mailnews/base/search/content/CustomHeaders.xul b/mailnews/base/search/content/CustomHeaders.xul
new file mode 100644
index 000000000..c13a8dba8
--- /dev/null
+++ b/mailnews/base/search/content/CustomHeaders.xul
@@ -0,0 +1,57 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/CustomHeaders.dtd">
+<dialog id="customHeadersDialog"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="onLoad();"
+ ondialogaccept="return onOk();"
+ ondialogextra1="onAddHeader();"
+ ondialogextra2="onRemoveHeader();"
+ style="width: 30em; height: 25em;"
+ persist="width height screenX screenY"
+ title="&window.title;"
+ buttons="accept,cancel,extra1,extra2">
+
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_filter" src="chrome://messenger/locale/filter.properties"/>
+ <stringbundle id="bundle_custom" src="chrome://messenger/locale/custom.properties"/>
+ </stringbundleset>
+
+ <script type="application/javascript" src="chrome://messenger/content/CustomHeaders.js"/>
+
+ <grid flex="1">
+ <columns>
+ <column flex="1"/>
+ <column/>
+ </columns>
+ <rows>
+ <row>
+ <label accesskey="&newMsgHeader.accesskey;" control="headerInput" value="&newMsgHeader.label;"/>
+ </row>
+ <row>
+ <textbox id="headerInput" onfocus="this.select();" oninput="onTextInput();"/>
+ </row>
+
+ <row flex="1">
+ <vbox>
+ <listbox id="headerList" flex="1" onselect="updateRemoveButton();" />
+ </vbox>
+
+ <vbox>
+ <button id="addButton"
+ label="&addButton.label;"
+ accesskey="&addButton.accesskey;"
+ dlgtype="extra1"/>
+ <button id="removeButton"
+ label="&removeButton.label;"
+ accesskey="&removeButton.accesskey;"
+ dlgtype="extra2"/>
+ </vbox>
+ </row>
+ </rows>
+ </grid>
+</dialog>
diff --git a/mailnews/base/search/content/FilterEditor.js b/mailnews/base/search/content/FilterEditor.js
new file mode 100644
index 000000000..986185d34
--- /dev/null
+++ b/mailnews/base/search/content/FilterEditor.js
@@ -0,0 +1,854 @@
+/* -*- 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();
+}
diff --git a/mailnews/base/search/content/FilterEditor.xul b/mailnews/base/search/content/FilterEditor.xul
new file mode 100644
index 000000000..0e322264d
--- /dev/null
+++ b/mailnews/base/search/content/FilterEditor.xul
@@ -0,0 +1,125 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/filterDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+
+<?xul-overlay href="chrome://messenger/content/searchTermOverlay.xul"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/FilterEditor.dtd">
+
+<dialog id="FilterEditor"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&window.title;"
+ style="&filterEditorDialog.dimensions;"
+ windowtype="mailnews:filtereditor"
+ persist="width height screenX screenY"
+ buttons="accept,cancel"
+ onload="filterEditorOnLoad();"
+ onunload="filterEditorOnUnload();"
+ ondialogaccept="return onAccept();">
+
+ <dummy class="usesMailWidgets"/>
+ <stringbundleset id="stringbundleset">
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+ <stringbundle id="bundle_filter" src="chrome://messenger/locale/filter.properties"/>
+ <stringbundle id="bundle_search" src="chrome://messenger/locale/search.properties"/>
+ </stringbundleset>
+
+ <script type="application/javascript" src="chrome://messenger/content/mailWindowOverlay.js"/>
+ <script type="application/javascript" src="chrome://messenger/content/mailCommands.js"/>
+ <script type="application/javascript" src="chrome://messenger/content/FilterEditor.js"/>
+
+ <commandset>
+ <command id="cmd_updateFilterType" oncommand="updateFilterType();"/>
+ <command id="cmd_updateClassificationMenu" oncommand="gFilterTypeSelector.updateClassificationMenu();"/>
+ </commandset>
+
+ <vbox>
+ <hbox align="center">
+ <label value="&filterName.label;" accesskey="&filterName.accesskey;" control="filterName"/>
+ <textbox flex="1" id="filterName"/>
+ <spacer flex="1"/>
+ </hbox>
+ </vbox>
+
+ <separator class="thin"/>
+
+ <vbox flex="1">
+ <groupbox>
+ <caption label="&contextDesc.label;"/>
+ <grid>
+ <columns>
+ <column/>
+ <column/>
+ </columns>
+ <rows>
+ <row>
+ <checkbox id="runManual"
+ label="&contextManual.label;"
+ accesskey="&contextManual.accesskey;"
+ command="cmd_updateFilterType"/>
+ </row>
+ <row>
+ <checkbox id="runIncoming"
+ label="&contextIncomingMail.label;"
+ accesskey="&contextIncomingMail.accesskey;"
+ command="cmd_updateClassificationMenu"/>
+ <menulist id="pluginsRunOrder"
+ command="cmd_updateFilterType">
+ <menupopup>
+ <menuitem id="runBeforePlugins"
+ label="&contextBeforeCls.label;"/>
+ <menuitem id="runAfterPlugins"
+ label="&contextAfterCls.label;"/>
+ </menupopup>
+ </menulist>
+ </row>
+ <row>
+ <checkbox id="runArchive"
+ label="&contextArchive.label;"
+ accesskey="&contextArchive.accesskey;"
+ command="cmd_updateFilterType"/>
+ </row>
+ <row>
+ <checkbox id="runOutgoing"
+ label="&contextOutgoing.label;"
+ accesskey="&contextOutgoing.accesskey;"
+ command="cmd_updateFilterType"/>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <vbox id="searchTermListBox" flex="1"/>
+ </vbox>
+
+ <splitter id="gray_horizontal_splitter" persist="state"/>
+
+ <vbox flex="1">
+ <label value="&filterActionDesc.label;"
+ accesskey="&filterActionDesc.accesskey;"
+ control="filterActionList"/>
+ <listbox id="filterActionList" flex="1" rows="4" minheight="35%"
+ onfocus="setLastActionFocus();" focusedAction="0">
+ <listcols>
+ <listcol flex="&filterActionTypeFlexValue;"/>
+ <listcol flex="&filterActionTargetFlexValue;"/>
+ <listcol class="filler"/>
+ </listcols>
+ </listbox>
+ </vbox>
+
+ <vbox id="statusbar" style="visibility: hidden;">
+ <hbox align="center">
+ <label>
+ &filterActionOrderWarning.label;
+ </label>
+ <label class="text-link" onclick="showActionsOrder();">&filterActionOrder.label;</label>
+ </hbox>
+ </vbox>
+</dialog>
diff --git a/mailnews/base/search/content/searchTermOverlay.js b/mailnews/base/search/content/searchTermOverlay.js
new file mode 100644
index 000000000..faf4ba91c
--- /dev/null
+++ b/mailnews/base/search/content/searchTermOverlay.js
@@ -0,0 +1,536 @@
+/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* 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/. */
+
+var gTotalSearchTerms=0;
+var gSearchTermList;
+var gSearchTerms = new Array;
+var gSearchRemovedTerms = new Array;
+var gSearchScope;
+var gSearchBooleanRadiogroup;
+
+var gUniqueSearchTermCounter = 0; // gets bumped every time we add a search term so we can always
+ // dynamically generate unique IDs for the terms.
+
+// cache these so we don't have to hit the string bundle for them
+var gMoreButtonTooltipText;
+var gLessButtonTooltipText;
+var gLoading = true;
+
+
+function searchTermContainer() {}
+
+searchTermContainer.prototype = {
+ internalSearchTerm : '',
+ internalBooleanAnd : '',
+
+ // this.searchTerm: the actual nsIMsgSearchTerm object
+ get searchTerm() { return this.internalSearchTerm; },
+ set searchTerm(val) {
+ this.internalSearchTerm = val;
+
+ var term = val;
+ // val is a nsIMsgSearchTerm
+ var searchAttribute=this.searchattribute;
+ var searchOperator=this.searchoperator;
+ var searchValue=this.searchvalue;
+
+ // now reflect all attributes of the searchterm into the widgets
+ if (searchAttribute)
+ {
+ // for custom, the value is the custom id, not the integer attribute
+ if (term.attrib == Components.interfaces.nsMsgSearchAttrib.Custom)
+ searchAttribute.value = term.customId;
+ else
+ searchAttribute.value = term.attrib;
+ }
+ if (searchOperator) searchOperator.value = val.op;
+ if (searchValue) searchValue.value = term.value;
+
+ this.booleanAnd = val.booleanAnd;
+ this.matchAll = val.matchAll;
+ return val;
+ },
+
+ // searchscope - just forward to the searchattribute
+ get searchScope() {
+ if (this.searchattribute)
+ return this.searchattribute.searchScope;
+ return undefined;
+ },
+ set searchScope(val) {
+ var searchAttribute = this.searchattribute;
+ if (searchAttribute) searchAttribute.searchScope=val;
+ return val;
+ },
+
+ saveId: function (element, slot) {
+ this[slot] = element.id;
+ },
+
+ getElement: function (slot) {
+ return document.getElementById(this[slot]);
+ },
+
+ // three well-defined properties:
+ // searchattribute, searchoperator, searchvalue
+ // the trick going on here is that we're storing the Element's Id,
+ // not the element itself, because the XBL object may change out
+ // from underneath us
+ get searchattribute() { return this.getElement("internalSearchAttributeId"); },
+ set searchattribute(val) {
+ this.saveId(val, "internalSearchAttributeId");
+ return val;
+ },
+ get searchoperator() { return this.getElement("internalSearchOperatorId"); },
+ set searchoperator(val) {
+ this.saveId(val, "internalSearchOperatorId");
+ return val;
+ },
+ get searchvalue() { return this.getElement("internalSearchValueId"); },
+ set searchvalue(val) {
+ this.saveId(val, "internalSearchValueId");
+ return val;
+ },
+
+ booleanNodes: null,
+ get booleanAnd() { return this.internalBooleanAnd; },
+ set booleanAnd(val) {
+ this.internalBooleanAnd = val;
+ return val;
+ },
+
+ save: function () {
+ var searchTerm = this.searchTerm;
+ var nsMsgSearchAttrib = Components.interfaces.nsMsgSearchAttrib;
+
+ if (isNaN(this.searchattribute.value)) // is this a custom term?
+ {
+ searchTerm.attrib = nsMsgSearchAttrib.Custom;
+ searchTerm.customId = this.searchattribute.value;
+ }
+ else
+ {
+ searchTerm.attrib = this.searchattribute.value;
+ }
+
+ if (this.searchattribute.value > nsMsgSearchAttrib.OtherHeader && this.searchattribute.value < nsMsgSearchAttrib.kNumMsgSearchAttributes)
+ searchTerm.arbitraryHeader = this.searchattribute.label;
+ searchTerm.op = this.searchoperator.value;
+ if (this.searchvalue.value)
+ this.searchvalue.save();
+ else
+ this.searchvalue.saveTo(searchTerm.value);
+ searchTerm.value = this.searchvalue.value;
+ searchTerm.booleanAnd = this.booleanAnd;
+ searchTerm.matchAll = this.matchAll;
+ },
+ // if you have a search term element with no search term
+ saveTo: function(searchTerm) {
+ this.internalSearchTerm = searchTerm;
+ this.save();
+ }
+}
+
+var nsIMsgSearchTerm = Components.interfaces.nsIMsgSearchTerm;
+
+function initializeSearchWidgets()
+{
+ gSearchBooleanRadiogroup = document.getElementById("booleanAndGroup");
+ gSearchTermList = document.getElementById("searchTermList");
+
+ // initialize some strings
+ var bundle = document.getElementById('bundle_search');
+ gMoreButtonTooltipText = bundle.getString('moreButtonTooltipText');
+ gLessButtonTooltipText = bundle.getString('lessButtonTooltipText');
+}
+
+function initializeBooleanWidgets()
+{
+ var booleanAnd = true;
+ var matchAll = false;
+ // get the boolean value from the first term
+ var firstTerm = gSearchTerms[0].searchTerm;
+ if (firstTerm)
+ {
+ // If there is a second term, it should actually define whether we're
+ // using 'and' or not. Note that our UI is not as rich as the
+ // underlying search model, so there's the potential to lose here when
+ // grouping is involved.
+ booleanAnd = (gSearchTerms.length > 1) ?
+ gSearchTerms[1].searchTerm.booleanAnd : firstTerm.booleanAnd;
+ matchAll = firstTerm.matchAll;
+ }
+ // target radio items have value="and" or value="or" or "all"
+ gSearchBooleanRadiogroup.value = matchAll
+ ? "matchAll"
+ : (booleanAnd ? "and" : "or")
+ var searchTerms = document.getElementById("searchTermList");
+ if (searchTerms)
+ updateSearchTermsListbox(matchAll);
+}
+
+function initializeSearchRows(scope, searchTerms)
+{
+ for (var i = 0; i < searchTerms.Count(); i++) {
+ var searchTerm = searchTerms.QueryElementAt(i, nsIMsgSearchTerm);
+ createSearchRow(i, scope, searchTerm, false);
+ gTotalSearchTerms++;
+ }
+ initializeBooleanWidgets();
+ updateRemoveRowButton();
+}
+
+/**
+ * Enables/disables all the visible elements inside the search terms listbox.
+ *
+ * @param matchAllValue boolean value from the first search term
+ */
+function updateSearchTermsListbox(matchAllValue)
+{
+ var searchTerms = document.getElementById("searchTermList");
+ searchTerms.setAttribute("disabled", matchAllValue);
+ var searchAttributeList = searchTerms.getElementsByTagName("searchattribute");
+ var searchOperatorList = searchTerms.getElementsByTagName("searchoperator");
+ var searchValueList = searchTerms.getElementsByTagName("searchvalue");
+ for (var i = 0; i < searchAttributeList.length; i++) {
+ searchAttributeList[i].setAttribute("disabled", matchAllValue);
+ searchOperatorList[i].setAttribute("disabled", matchAllValue);
+ searchValueList[i].setAttribute("disabled", matchAllValue);
+ if (!matchAllValue)
+ searchValueList[i].removeAttribute("disabled");
+ }
+ var moreOrLessButtonsList = searchTerms.getElementsByTagName("button");
+ for (var i = 0; i < moreOrLessButtonsList.length; i++) {
+ moreOrLessButtonsList[i].setAttribute("disabled", matchAllValue);
+ }
+ if (!matchAllValue)
+ updateRemoveRowButton();
+}
+
+// enables/disables the less button for the first row of search terms.
+function updateRemoveRowButton()
+{
+ var firstListItem = gSearchTermList.getItemAtIndex(0);
+ if (firstListItem)
+ firstListItem.lastChild.lastChild.setAttribute("disabled", gTotalSearchTerms == 1);
+}
+
+// Returns the actual list item row index in the list of search rows
+// that contains the passed in element id.
+function getSearchRowIndexForElement(aElement)
+{
+ var listItem = aElement;
+
+ while (listItem && listItem.localName != "listitem")
+ listItem = listItem.parentNode;
+
+ return gSearchTermList.getIndexOfItem(listItem);
+}
+
+function onMore(event)
+{
+ // if we have an event, extract the list row index and use that as the row number
+ // for our insertion point. If there is no event, append to the end....
+ var rowIndex;
+
+ if (event)
+ rowIndex = getSearchRowIndexForElement(event.target) + 1;
+ else
+ rowIndex = gSearchTermList.getRowCount();
+
+ createSearchRow(rowIndex, gSearchScope, null, event != null);
+ gTotalSearchTerms++;
+ updateRemoveRowButton();
+
+ // the user just added a term, so scroll to it
+ gSearchTermList.ensureIndexIsVisible(rowIndex);
+}
+
+function onLess(event)
+{
+ if (event && gTotalSearchTerms > 1)
+ {
+ removeSearchRow(getSearchRowIndexForElement(event.target));
+ --gTotalSearchTerms;
+ }
+
+ updateRemoveRowButton();
+}
+
+// set scope on all visible searchattribute tags
+function setSearchScope(scope)
+{
+ gSearchScope = scope;
+ for (var i = 0; i < gSearchTerms.length; i++)
+ {
+ // don't set element attributes if XBL hasn't loaded
+ if (!(gSearchTerms[i].obj.searchattribute.searchScope === undefined))
+ {
+ gSearchTerms[i].obj.searchattribute.searchScope = scope;
+ // act like the user "selected" this, see bug #202848
+ gSearchTerms[i].obj.searchattribute.onSelect(null /* no event */);
+ }
+ gSearchTerms[i].scope = scope;
+ }
+}
+
+function updateSearchAttributes()
+{
+ for (var i=0; i<gSearchTerms.length; i++)
+ gSearchTerms[i].obj.searchattribute.refreshList();
+ }
+
+function booleanChanged(event) {
+ // when boolean changes, we have to update all the attributes on the search terms
+ var newBoolValue = (event.target.getAttribute("value") == "and");
+ var matchAllValue = (event.target.getAttribute("value") == "matchAll");
+ if (document.getElementById("abPopup")) {
+ var selectedAB = document.getElementById("abPopup").selectedItem.value;
+ setSearchScope(GetScopeForDirectoryURI(selectedAB));
+ }
+ for (var i=0; i<gSearchTerms.length; i++) {
+ let searchTerm = gSearchTerms[i].obj;
+ // If term is not yet initialized in the UI, change the original object.
+ if (!searchTerm || !gSearchTerms[i].initialized)
+ searchTerm = gSearchTerms[i].searchTerm;
+
+ searchTerm.booleanAnd = newBoolValue;
+ searchTerm.matchAll = matchAllValue;
+ }
+ var searchTerms = document.getElementById("searchTermList");
+ if (searchTerms)
+ {
+ if (!matchAllValue && searchTerms.hidden && !gTotalSearchTerms)
+ onMore(null); // fake to get empty row.
+ updateSearchTermsListbox(matchAllValue);
+ }
+}
+
+/**
+ * Create a new search row with all the needed elements.
+ *
+ * @param index index of the position in the menulist where to add the row
+ * @param scope a nsMsgSearchScope constant indicating scope of this search rule
+ * @param searchTerm nsIMsgSearchTerm object to hold the search term
+ * @param aUserAdded boolean indicating if the row addition was initiated by the user
+ * (e.g. via the '+' button)
+ */
+function createSearchRow(index, scope, searchTerm, aUserAdded)
+{
+ var searchAttr = document.createElement("searchattribute");
+ var searchOp = document.createElement("searchoperator");
+ var searchVal = document.createElement("searchvalue");
+
+ var moreButton = document.createElement("button");
+ var lessButton = document.createElement("button");
+ moreButton.setAttribute("class", "small-button");
+ moreButton.setAttribute("oncommand", "onMore(event);");
+ moreButton.setAttribute('label', '+');
+ moreButton.setAttribute('tooltiptext', gMoreButtonTooltipText);
+ lessButton.setAttribute("class", "small-button");
+ lessButton.setAttribute("oncommand", "onLess(event);");
+ lessButton.setAttribute('label', '\u2212');
+ lessButton.setAttribute('tooltiptext', gLessButtonTooltipText);
+
+ // now set up ids:
+ searchAttr.id = "searchAttr" + gUniqueSearchTermCounter;
+ searchOp.id = "searchOp" + gUniqueSearchTermCounter;
+ searchVal.id = "searchVal" + gUniqueSearchTermCounter;
+
+ searchAttr.setAttribute("for", searchOp.id + "," + searchVal.id);
+ searchOp.setAttribute("opfor", searchVal.id);
+
+ var rowdata = [searchAttr, searchOp, searchVal,
+ [moreButton, lessButton] ];
+ var searchrow = constructRow(rowdata);
+ searchrow.id = "searchRow" + gUniqueSearchTermCounter;
+
+ var searchTermObj = new searchTermContainer;
+ searchTermObj.searchattribute = searchAttr;
+ searchTermObj.searchoperator = searchOp;
+ searchTermObj.searchvalue = searchVal;
+
+ // now insert the new search term into our list of terms
+ gSearchTerms.splice(index, 0, {obj:searchTermObj, scope:scope, searchTerm:searchTerm, initialized:false});
+
+ var editFilter = null;
+ try { editFilter = gFilter; } catch(e) { }
+
+ var editMailView = null;
+ try { editMailView = gMailView; } catch(e) { }
+
+ if ((!editFilter && !editMailView) ||
+ (editFilter && index == gTotalSearchTerms) ||
+ (editMailView && index == gTotalSearchTerms))
+ gLoading = false;
+
+ // index is index of new row
+ // gTotalSearchTerms has not been updated yet
+ if (gLoading || index == gTotalSearchTerms) {
+ gSearchTermList.appendChild(searchrow);
+ }
+ else {
+ var currentItem = gSearchTermList.getItemAtIndex(index);
+ gSearchTermList.insertBefore(searchrow, currentItem);
+ }
+
+ // If this row was added by user action, focus the value field.
+ if (aUserAdded) {
+ document.commandDispatcher.advanceFocusIntoSubtree(searchVal);
+ searchrow.setAttribute("highlight", "true");
+ }
+
+ // bump our unique search term counter
+ gUniqueSearchTermCounter++;
+}
+
+function initializeTermFromId(id)
+{
+ initializeTermFromIndex(getSearchRowIndexForElement(document.getElementById(id)));
+}
+
+function initializeTermFromIndex(index)
+{
+ var searchTermObj = gSearchTerms[index].obj;
+
+ searchTermObj.searchScope = gSearchTerms[index].scope;
+ // the search term will initialize the searchTerm element, including
+ // .booleanAnd
+ if (gSearchTerms[index].searchTerm)
+ searchTermObj.searchTerm = gSearchTerms[index].searchTerm;
+ // here, we don't have a searchTerm, so it's probably a new element -
+ // we'll initialize the .booleanAnd from the existing setting in
+ // the UI
+ else
+ {
+ searchTermObj.booleanAnd = (gSearchBooleanRadiogroup.value == "and");
+ if (index)
+ {
+ // If we weren't pre-initialized with a searchTerm then steal the
+ // search attribute and operator from the previous row.
+ searchTermObj.searchattribute.value = gSearchTerms[index - 1].obj.searchattribute.value;
+ searchTermObj.searchoperator.value = gSearchTerms[index - 1].obj.searchoperator.value;
+ }
+ }
+
+ gSearchTerms[index].initialized = true;
+}
+
+/**
+ * Creates a <listitem> using the array children as the children
+ * of each listcell.
+ * @param aChildren An array of XUL elements to put into the listitem.
+ * Each array member is put into a separate listcell.
+ * If the member itself is an array of elements,
+ * all of them are put into the same listcell.
+ */
+function constructRow(aChildren)
+{
+ let listitem = document.createElement("listitem");
+ listitem.setAttribute("allowevents", "true");
+ for (let i = 0; i < aChildren.length; i++) {
+ let listcell = document.createElement("listcell");
+ let child = aChildren[i];
+
+ if (child instanceof Array) {
+ for (let j = 0; j < child.length; j++)
+ listcell.appendChild(child[j]);
+ } else {
+ child.setAttribute("flex", "1");
+ listcell.appendChild(child);
+ }
+ listitem.appendChild(listcell);
+ }
+ return listitem;
+}
+
+function removeSearchRow(index)
+{
+ var searchTermObj = gSearchTerms[index].obj;
+ if (!searchTermObj) {
+ return;
+ }
+
+ // if it is an existing (but offscreen) term,
+ // make sure it is initialized before we remove it.
+ if (!gSearchTerms[index].searchTerm && !gSearchTerms[index].initialized)
+ initializeTermFromIndex(index);
+
+ // need to remove row from list, so walk upwards from the
+ // searchattribute to find the first <listitem>
+ var listitem = searchTermObj.searchattribute;
+
+ while (listitem) {
+ if (listitem.localName == "listitem") break;
+ listitem = listitem.parentNode;
+ }
+
+ if (!listitem) {
+ dump("Error: couldn't find parent listitem!\n");
+ return;
+ }
+
+
+ if (searchTermObj.searchTerm) {
+ gSearchRemovedTerms[gSearchRemovedTerms.length] = searchTermObj.searchTerm;
+ } else {
+ //dump("That wasn't real. ignoring \n");
+ }
+
+ listitem.remove();
+
+ // now remove the item from our list of terms
+ gSearchTerms.splice(index, 1);
+}
+
+// save the search terms from the UI back to the actual search terms
+// searchTerms: nsISupportsArray of terms
+// termOwner: object which can contain and create the terms
+// (will be unnecessary if we just make terms creatable
+// via XPCOM)
+function saveSearchTerms(searchTerms, termOwner)
+{
+ var matchAll = gSearchBooleanRadiogroup.value == 'matchAll';
+ var i;
+ for (i = 0; i < gSearchRemovedTerms.length; i++)
+ searchTerms.RemoveElement(gSearchRemovedTerms[i]);
+
+ for (i = 0; i < gSearchTerms.length; i++) {
+ try {
+ gSearchTerms[i].obj.matchAll = matchAll;
+ var searchTerm = gSearchTerms[i].obj.searchTerm;
+ if (searchTerm) {
+ gSearchTerms[i].obj.save();
+ } else if (!gSearchTerms[i].initialized) {
+ // the term might be an offscreen one we haven't initialized yet
+ searchTerm = gSearchTerms[i].searchTerm;
+ } else {
+ // need to create a new searchTerm, and somehow save it to that
+ searchTerm = termOwner.createTerm();
+ gSearchTerms[i].obj.saveTo(searchTerm);
+ // this might not be the right place for the term,
+ // but we need to make the array longer anyway
+ termOwner.appendTerm(searchTerm);
+ }
+ searchTerms.SetElementAt(i, searchTerm);
+ } catch (ex) {
+ dump("** Error saving element " + i + ": " + ex + "\n");
+ }
+ }
+}
+
+function onReset(event)
+{
+ while (gTotalSearchTerms>0)
+ removeSearchRow(--gTotalSearchTerms);
+ onMore(null);
+}
+
+function hideMatchAllItem()
+{
+ var allItems = document.getElementById('matchAllItem');
+ if (allItems)
+ allItems.hidden = true;
+}
diff --git a/mailnews/base/search/content/searchTermOverlay.xul b/mailnews/base/search/content/searchTermOverlay.xul
new file mode 100644
index 000000000..c14e05cb0
--- /dev/null
+++ b/mailnews/base/search/content/searchTermOverlay.xul
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE overlay SYSTEM "chrome://messenger/locale/searchTermOverlay.dtd">
+
+<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script type="application/javascript"
+ src="chrome://messenger/content/searchTermOverlay.js"/>
+ <script type="application/javascript"
+ src="chrome://messenger/content/dateFormat.js"/>
+
+ <vbox id="searchTermListBox">
+
+ <radiogroup id="booleanAndGroup" orient="horizontal" value="and"
+ oncommand="booleanChanged(event);">
+ <radio value="and" label="&matchAll.label;"
+ accesskey="&matchAll.accesskey;"/>
+ <radio value="or" label="&matchAny.label;"
+ accesskey="&matchAny.accesskey;"/>
+ <radio value="matchAll" id="matchAllItem" label="&matchAllMsgs.label;"
+ accesskey="&matchAllMsgs.accesskey;"/>
+ </radiogroup>
+
+ <hbox flex="1">
+ <hbox id="searchterms"/>
+ <listbox flex="1" id="searchTermList" rows="4" minheight="35%">
+ <listcols>
+ <listcol flex="&searchTermListAttributesFlexValue;"/>
+ <listcol flex="&searchTermListOperatorsFlexValue;"/>
+ <listcol flex="&searchTermListValueFlexValue;"/>
+ <listcol class="filler"/>
+ </listcols>
+
+ <!-- this is what the listitems will look like:
+ <listitem id="searchListItem">
+ <listcell allowevents="true">
+ <searchattribute id="searchAttr1" for="searchOp1,searchValue1" flex="1"/>
+ </listcell>
+ <listcell allowevents="true">
+ <searchoperator id="searchOp1" opfor="searchValue1" flex="1"/>
+ </listcell>
+ <listcell allowevents="true" >
+ <searchvalue id="searchValue1" flex="1"/>
+ </listcell>
+ <listcell>
+ <button label="add"/>
+ <button label="remove"/>
+ </listcell>
+ </listitem>
+ <listitem>
+ <listcell label="the.."/>
+ <listcell label="contains.."/>
+ <listcell label="text here"/>
+ <listcell label="+/-"/>
+ </listitem>
+ -->
+ </listbox>
+
+ </hbox>
+ </vbox>
+
+</overlay>
diff --git a/mailnews/base/search/content/searchWidgets.xml b/mailnews/base/search/content/searchWidgets.xml
new file mode 100644
index 000000000..80ebe38c4
--- /dev/null
+++ b/mailnews/base/search/content/searchWidgets.xml
@@ -0,0 +1,738 @@
+<?xml version="1.0"?>
+
+<!-- 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 file has the following external dependencies:
+ -gFilterActionStrings from FilterEditor.js
+ -gFilterList from FilterEditor.js
+ -gFilter from FilterEditor.js
+ -gCustomActions from FilterEditor.js
+ -gFilterType from FilterEditor.js
+ -checkActionsReorder from FilterEditor.js
+-->
+
+<!DOCTYPE dialog [
+ <!ENTITY % filterEditorDTD SYSTEM "chrome://messenger/locale/FilterEditor.dtd" >
+%filterEditorDTD;
+ <!ENTITY % messengerDTD SYSTEM "chrome://messenger/locale/messenger.dtd" >
+%messengerDTD;
+]>
+
+<bindings id="filterBindings"
+ xmlns="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:nc="http://home.netscape.com/NC-rdf#"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+
+ <binding id="ruleactiontype-menulist">
+ <content>
+ <xul:menulist class="ruleaction-type">
+ <xul:menupopup>
+ <xul:menuitem label="&moveMessage.label;" value="movemessage" enablefornews="false"/>
+ <xul:menuitem label="&copyMessage.label;" value="copymessage"/>
+ <xul:menuseparator enablefornews="false"/>
+ <xul:menuitem label="&forwardTo.label;" value="forwardmessage" enablefornews="false"/>
+ <xul:menuitem label="&replyWithTemplate.label;" value="replytomessage" enablefornews="false"/>
+ <xul:menuseparator/>
+ <xul:menuitem label="&markMessageRead.label;" value="markasread"/>
+ <xul:menuitem label="&markMessageUnread.label;" value="markasunread"/>
+ <xul:menuitem label="&markMessageStarred.label;" value="markasflagged"/>
+ <xul:menuitem label="&setPriority.label;" value="setpriorityto"/>
+ <xul:menuitem label="&addTag.label;" value="addtagtomessage"/>
+ <xul:menuitem label="&setJunkScore.label;" value="setjunkscore" enablefornews="false"/>
+ <xul:menuseparator enableforpop3="true"/>
+ <xul:menuitem label="&deleteMessage.label;" value="deletemessage"/>
+ <xul:menuitem label="&deleteFromPOP.label;" value="deletefrompopserver" enableforpop3="true"/>
+ <xul:menuitem label="&fetchFromPOP.label;" value="fetchfrompopserver" enableforpop3="true"/>
+ <xul:menuseparator/>
+ <xul:menuitem label="&ignoreThread.label;" value="ignorethread"/>
+ <xul:menuitem label="&ignoreSubthread.label;" value="ignoresubthread"/>
+ <xul:menuitem label="&watchThread.label;" value="watchthread"/>
+ <xul:menuseparator/>
+ <xul:menuitem label="&stopExecution.label;" value="stopexecution"/>
+ </xul:menupopup>
+ </xul:menulist>
+ </content>
+
+ <implementation>
+ <constructor>
+ <![CDATA[
+ this.addCustomActions();
+ this.hideInvalidActions();
+ // differentiate between creating a new, next available action,
+ // and creating a row which will be initialized with an action
+ if (!this.parentNode.hasAttribute('initialActionIndex'))
+ {
+ var unavailableActions = this.usedActionsList();
+ // select the first one that's not in the list
+ for (var index = 0; index < this.menuitems.length; index++)
+ {
+ var menu = this.menuitems[index];
+ if (!(menu.value in unavailableActions) && !menu.hidden)
+ {
+ this.menulist.value = menu.value;
+ this.parentNode.setAttribute('value', menu.value);
+ break;
+ }
+ }
+ }
+ else
+ {
+ this.parentNode.mActionTypeInitialized = true;
+ this.parentNode.clearInitialActionIndex();
+ }
+ ]]>
+ </constructor>
+
+ <field name="menulist">document.getAnonymousNodes(this)[0]</field>
+ <field name="menuitems">this.menulist.getElementsByTagNameNS(this.menulist.namespaceURI, 'menuitem')</field>
+
+ <method name="hideInvalidActions">
+ <body>
+ <![CDATA[
+ let menupopup = this.menulist.menupopup;
+ let scope = getScopeFromFilterList(gFilterList);
+
+ // walk through the list of filter actions and hide any actions which aren't valid
+ // for our given scope (news, imap, pop, etc) and context
+ let elements, i;
+
+ // disable / enable all elements in the "filteractionlist"
+ // based on the scope and the "enablefornews" attribute
+ elements = menupopup.getElementsByAttribute("enablefornews", "true");
+ for (i = 0; i < elements.length; i++)
+ elements[i].hidden = scope != Components.interfaces.nsMsgSearchScope.newsFilter;
+
+ elements = menupopup.getElementsByAttribute("enablefornews", "false");
+ for (i = 0; i < elements.length; i++)
+ elements[i].hidden = scope == Components.interfaces.nsMsgSearchScope.newsFilter;
+
+ elements = menupopup.getElementsByAttribute("enableforpop3", "true");
+ for (i = 0; i < elements.length; i++)
+ elements[i].hidden = !((gFilterList.folder.server.type == "pop3") ||
+ (gFilterList.folder.server.type == "none"));
+
+ elements = menupopup.getElementsByAttribute("isCustom", "true");
+ // Note there might be an additional element here as a placeholder
+ // for a missing action, so we iterate over the known actions
+ // instead of the elements.
+ for (i = 0; i < gCustomActions.length; i++)
+ elements[i].hidden = !gCustomActions[i]
+ .isValidForType(gFilterType, scope);
+
+ // Disable "Reply with Template" if there are no templates.
+ if (!this.getTemplates(false)) {
+ elements = menupopup.getElementsByAttribute("value", "replytomessage");
+ if (elements.length == 1)
+ elements[0].hidden = true;
+ }
+ ]]>
+ </body>
+ </method>
+
+ <method name="addCustomActions">
+ <body>
+ <![CDATA[
+ var menupopup = this.menulist.menupopup;
+ for (var i = 0; i < gCustomActions.length; i++)
+ {
+ var customAction = gCustomActions[i];
+ var menuitem = document.createElementNS(
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "xul:menuitem");
+ menuitem.setAttribute("label", customAction.name);
+ menuitem.setAttribute("value", customAction.id);
+ menuitem.setAttribute("isCustom", "true");
+ menupopup.appendChild(menuitem);
+ }
+ ]]>
+ </body>
+ </method>
+
+ <!-- returns a hash containing all of the filter actions which are currently being used by other filteractionrows -->
+ <method name="usedActionsList">
+ <body>
+ <![CDATA[
+ var usedActions = {};
+ var currentFilterActionRow = this.parentNode;
+ var listBox = currentFilterActionRow.mListBox; // need to account for the list item
+ // now iterate over each list item in the list box
+ for (var index = 0; index < listBox.getRowCount(); index++)
+ {
+ var filterActionRow = listBox.getItemAtIndex(index);
+ if (filterActionRow != currentFilterActionRow)
+ {
+ var actionValue = filterActionRow.getAttribute('value');
+
+ // let custom actions decide if dups are allowed
+ var isCustom = false;
+ for (var i = 0; i < gCustomActions.length; i++)
+ {
+ if (gCustomActions[i].id == actionValue)
+ {
+ isCustom = true;
+ if (!gCustomActions[i].allowDuplicates)
+ usedActions[actionValue] = true;
+ break;
+ }
+ }
+
+ if (!isCustom) {
+ // The following actions can appear more than once in a single filter
+ // so do not set them as already used.
+ if (actionValue != 'addtagtomessage' &&
+ actionValue != 'forwardmessage' &&
+ actionValue != 'copymessage')
+ usedActions[actionValue] = true;
+ // If either Delete message or Move message exists, disable the other one.
+ // It does not make sense to apply both to the same message.
+ if (actionValue == 'deletemessage')
+ usedActions['movemessage'] = true;
+ else if (actionValue == 'movemessage')
+ usedActions['deletemessage'] = true;
+ // The same with Mark as read/Mark as Unread.
+ else if (actionValue == 'markasread')
+ usedActions['markasunread'] = true;
+ else if (actionValue == 'markasunread')
+ usedActions['markasread'] = true;
+ }
+ }
+ }
+ return usedActions;
+ ]]>
+ </body>
+ </method>
+
+ <!--
+ - Check if there exist any templates in this account.
+ -
+ - @param populateTemplateList If true, create menuitems representing
+ - the found templates.
+ - @param templateMenuList The menulist element to create items in.
+ -
+ - @return True if at least one template was found, otherwise false.
+ -->
+ <method name="getTemplates">
+ <parameter name="populateTemplateList"/>
+ <parameter name="templateMenuList"/>
+ <body>
+ <![CDATA[
+ Components.utils.import("resource:///modules/iteratorUtils.jsm", this);
+ let identitiesRaw = MailServices.accounts
+ .getIdentitiesForServer(gFilterList.folder.server);
+ let identities = Array.from(this.fixIterator(identitiesRaw,
+ Components.interfaces.nsIMsgIdentity));
+
+ if (!identities.length) // typically if this is Local Folders
+ identities.push(MailServices.accounts.defaultAccount.defaultIdentity);
+
+ let templateFound = false;
+ let foldersScanned = [];
+
+ for (let identity of identities) {
+ let enumerator = null;
+ let msgFolder;
+ try {
+ msgFolder = Components.classes["@mozilla.org/rdf/rdf-service;1"]
+ .getService(Components.interfaces.nsIRDFService)
+ .GetResource(identity.stationeryFolder)
+ .QueryInterface(Components.interfaces.nsIMsgFolder);
+ // If we already processed this folder, do not set enumerator
+ // so that we skip this identity.
+ if (foldersScanned.indexOf(msgFolder) == -1) {
+ foldersScanned.push(msgFolder);
+ enumerator = msgFolder.msgDatabase.EnumerateMessages();
+ }
+ } catch (e) {
+ // The Templates folder may not exist, that is OK.
+ }
+
+ if (!enumerator)
+ continue;
+
+ while (enumerator.hasMoreElements()) {
+ let header = enumerator.getNext();
+ if (header instanceof Components.interfaces.nsIMsgDBHdr) {
+ templateFound = true;
+ if (!populateTemplateList)
+ return true;
+ let msgTemplateUri = msgFolder.URI + "?messageId=" +
+ header.messageId + '&subject=' + header.mime2DecodedSubject;
+ let newItem = templateMenuList.appendItem(header.mime2DecodedSubject,
+ msgTemplateUri);
+ }
+ }
+ }
+
+ return templateFound;
+ ]]>
+ </body>
+ </method>
+
+ </implementation>
+
+ <handlers>
+ <handler event="command">
+ <![CDATA[
+ this.parentNode.setAttribute('value', this.menulist.value);
+ checkActionsReorder();
+ ]]>
+ </handler>
+
+ <handler event="popupshowing">
+ <![CDATA[
+ var unavailableActions = this.usedActionsList();
+ for (var index = 0; index < this.menuitems.length; index++)
+ {
+ var menu = this.menuitems[index];
+ menu.setAttribute('disabled', menu.value in unavailableActions);
+ }
+ ]]>
+ </handler>
+ </handlers>
+ </binding>
+
+ <!-- This binding exists to disable the default binding of a listitem
+ in the search terms. -->
+ <binding id="listitem">
+ <implementation>
+ <method name="_fireEvent">
+ <parameter name="aName"/>
+ <body>
+ <![CDATA[
+ /* This provides a dummy _fireEvent function that
+ the listbox expects to be able to call.
+ See bug 202036. */
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+ <binding id="ruleaction" extends="#listitem">
+ <content allowevents="true">
+ <xul:listcell class="ruleactiontype"
+ orient="vertical" align="stretch" pack="center"/>
+ <xul:listcell class="ruleactiontarget" xbl:inherits="type=value"
+ orient="vertical" align="stretch" pack="center"/>
+ <xul:listcell>
+ <xul:button class="small-button"
+ label="+"
+ tooltiptext="&addAction.tooltip;"
+ oncommand="this.parentNode.parentNode.addRow();"/>
+ <xul:button class="small-button"
+ label="&#x2212;"
+ tooltiptext="&removeAction.tooltip;"
+ oncommand="this.parentNode.parentNode.removeRow();"
+ anonid="removeButton"/>
+ </xul:listcell>
+ </content>
+
+ <implementation>
+ <field name="mListBox">this.parentNode</field>
+ <field name="mRemoveButton">document.getAnonymousElementByAttribute(this, "anonid", "removeButton")</field>
+ <field name="mActionTypeInitialized">false</field>
+ <field name="mRuleActionTargetInitialized">false</field>
+ <field name="mRuleActionType">document.getAnonymousNodes(this)[0]</field>
+
+ <method name="clearInitialActionIndex">
+ <body>
+ <![CDATA[
+ // we should only remove the initialActionIndex after we have been told that
+ // both the rule action type and the rule action target have both been built since they both need
+ // this piece of information. This complication arises because both of these child elements are getting
+ // bound asynchronously after the search row has been constructed
+
+ if (this.mActionTypeInitialized && this.mRuleActionTargetInitialized)
+ this.removeAttribute('initialActionIndex');
+ ]]>
+ </body>
+ </method>
+
+ <method name="initWithAction">
+ <parameter name="aFilterAction"/>
+ <body>
+ <![CDATA[
+ var filterActionStr;
+ var actionTarget = document.getAnonymousNodes(this)[1];
+ var actionItem = document.getAnonymousNodes(actionTarget);
+ var nsMsgFilterAction = Components.interfaces.nsMsgFilterAction;
+ switch (aFilterAction.type)
+ {
+ case nsMsgFilterAction.Custom:
+ filterActionStr = aFilterAction.customId;
+ if (actionItem)
+ actionItem[0].value = aFilterAction.strValue;
+
+ // Make sure the custom action has been added. If not, it
+ // probably was from an extension that has been removed. We'll
+ // show a dummy menuitem to warn the user.
+ var needCustomLabel = true;
+ for (var i = 0; i < gCustomActions.length; i++)
+ {
+ if (gCustomActions[i].id == filterActionStr)
+ {
+ needCustomLabel = false;
+ break;
+ }
+ }
+ if (needCustomLabel)
+ {
+ var menuitem = document.createElementNS(
+ "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
+ "xul:menuitem");
+ menuitem.setAttribute("label",
+ gFilterBundle.getString("filterMissingCustomAction"));
+ menuitem.setAttribute("value", filterActionStr);
+ menuitem.disabled = true;
+ this.mRuleActionType.menulist.menupopup.appendChild(menuitem);
+ var scriptError = Components.classes["@mozilla.org/scripterror;1"]
+ .createInstance(Components.interfaces.nsIScriptError);
+ scriptError.init("Missing custom action " + filterActionStr,
+ null, null, 0, 0,
+ Components.interfaces.nsIScriptError.errorFlag,
+ "component javascript");
+ Services.console.logMessage(scriptError);
+ }
+ break;
+ case nsMsgFilterAction.MoveToFolder:
+ case nsMsgFilterAction.CopyToFolder:
+ actionItem[0].value = aFilterAction.targetFolderUri;
+ break;
+ case nsMsgFilterAction.Reply:
+ case nsMsgFilterAction.Forward:
+ actionItem[0].value = aFilterAction.strValue;
+ break;
+ case nsMsgFilterAction.Label:
+ actionItem[0].value = aFilterAction.label;
+ break;
+ case nsMsgFilterAction.ChangePriority:
+ actionItem[0].value = aFilterAction.priority;
+ break;
+ case nsMsgFilterAction.JunkScore:
+ actionItem[0].value = aFilterAction.junkScore;
+ break;
+ case nsMsgFilterAction.AddTag:
+ actionItem[0].value = aFilterAction.strValue;
+ break;
+ default:
+ break;
+ }
+ if (aFilterAction.type != nsMsgFilterAction.Custom)
+ filterActionStr = gFilterActionStrings[aFilterAction.type];
+ document.getAnonymousNodes(this.mRuleActionType)[0]
+ .value = filterActionStr;
+ this.mRuleActionTargetInitialized = true;
+ this.clearInitialActionIndex();
+ checkActionsReorder();
+ ]]>
+ </body>
+ </method>
+
+ <method name="validateAction">
+ <body>
+ <![CDATA[
+ // returns true if this row represents a valid filter action and false otherwise.
+ // This routine also prompts the user.
+ Components.utils.import("resource:///modules/MailUtils.js", this);
+ var filterActionString = this.getAttribute('value');
+ var actionTarget = document.getAnonymousNodes(this)[1];
+ var errorString, customError;
+
+ switch (filterActionString)
+ {
+ case "movemessage":
+ case "copymessage":
+ let msgFolder = document.getAnonymousNodes(actionTarget)[0].value ?
+ this.MailUtils.getFolderForURI(document.getAnonymousNodes(actionTarget)[0].value) : null;
+ if (!msgFolder || !msgFolder.canFileMessages)
+ errorString = "mustSelectFolder";
+ break;
+ case "forwardmessage":
+ if (document.getAnonymousNodes(actionTarget)[0].value.length < 3 ||
+ document.getAnonymousNodes(actionTarget)[0].value.indexOf('@') < 1)
+ errorString = "enterValidEmailAddress";
+ break;
+ case "replytomessage":
+ if (!document.getAnonymousNodes(actionTarget)[0].selectedItem)
+ errorString = "pickTemplateToReplyWith";
+ break;
+ default:
+ // some custom actions have no action value node
+ if (!document.getAnonymousNodes(actionTarget))
+ return true;
+ // locate the correct custom action, and check validity
+ for (var i = 0; i < gCustomActions.length; i++)
+ if (gCustomActions[i].id == filterActionString)
+ {
+ customError =
+ gCustomActions[i].validateActionValue(
+ document.getAnonymousNodes(actionTarget)[0].value,
+ gFilterList.folder, gFilterType);
+ break;
+ }
+ break;
+ }
+
+ errorString = errorString ?
+ gFilterBundle.getString(errorString) :
+ customError;
+ if (errorString)
+ Services.prompt.alert(window, null, errorString);
+
+ return !errorString;
+ ]]>
+ </body>
+ </method>
+
+ <method name="saveToFilter">
+ <parameter name="aFilter"/>
+ <body>
+ <![CDATA[
+ // create a new filter action, fill it in, and then append it to the filter
+ var filterAction = aFilter.createAction();
+ var filterActionString = this.getAttribute('value');
+ filterAction.type = gFilterActionStrings.indexOf(filterActionString);
+ var actionTarget = document.getAnonymousNodes(this)[1];
+ var actionItem = document.getAnonymousNodes(actionTarget);
+ var nsMsgFilterAction = Components.interfaces.nsMsgFilterAction;
+ switch (filterAction.type)
+ {
+ case nsMsgFilterAction.Label:
+ filterAction.label = actionItem[0].getAttribute("value");
+ break;
+ case nsMsgFilterAction.ChangePriority:
+ filterAction.priority = actionItem[0].getAttribute("value");
+ break;
+ case nsMsgFilterAction.MoveToFolder:
+ case nsMsgFilterAction.CopyToFolder:
+ filterAction.targetFolderUri = actionItem[0].value;
+ break;
+ case nsMsgFilterAction.JunkScore:
+ filterAction.junkScore = actionItem[0].value;
+ break;
+ case nsMsgFilterAction.Custom:
+ filterAction.customId = filterActionString;
+ // fall through to set the value
+ default:
+ if (actionItem)
+ filterAction.strValue = actionItem[0].value;
+ break;
+ }
+ aFilter.appendAction(filterAction);
+ ]]>
+ </body>
+ </method>
+
+ <method name="getActionStrings">
+ <parameter name="aActionStrings"/>
+ <body>
+ <![CDATA[
+ // Collect the action names and arguments in a plain string form.
+ let actionTarget = document.getAnonymousNodes(this)[1];
+ let actionItem = document.getAnonymousNodes(actionTarget);
+
+ aActionStrings.push({
+ label: document.getAnonymousNodes(this.mRuleActionType)[0].label,
+ argument: actionItem ?
+ (actionItem[0].label ?
+ actionItem[0].label : actionItem[0].value) : ""
+ });
+ ]]>
+ </body>
+ </method>
+
+ <method name="updateRemoveButton">
+ <body>
+ <![CDATA[
+ // if we only have one row of actions, then disable the remove button for that row
+ this.mListBox.getItemAtIndex(0).mRemoveButton.disabled = this.mListBox.getRowCount() == 1;
+ ]]>
+ </body>
+ </method>
+
+ <method name="addRow">
+ <body>
+ <![CDATA[
+ let listItem = document.createElement('listitem');
+ listItem.className = 'ruleaction';
+ listItem.setAttribute('onfocus','this.storeFocus();');
+ this.mListBox.insertBefore(listItem, this.nextSibling);
+ this.mListBox.ensureElementIsVisible(listItem);
+
+ // make sure the first remove button is enabled
+ this.updateRemoveButton();
+ checkActionsReorder();
+ ]]>
+ </body>
+ </method>
+
+ <method name="removeRow">
+ <body>
+ <![CDATA[
+ // this.mListBox will fail after the row is removed, so save it
+ let listBox = this.mListBox;
+ if (listBox.getRowCount() > 1)
+ this.remove();
+ // can't use 'this' as it is destroyed now
+ listBox.getItemAtIndex(0).updateRemoveButton();
+ checkActionsReorder();
+ ]]>
+ </body>
+ </method>
+
+ <method name="storeFocus">
+ <body>
+ <![CDATA[
+ // When this action row is focused, store its index in the parent listbox.
+ this.mListBox.setAttribute("focusedAction", this.mListBox.getIndexOfItem(this));
+ ]]>
+ </body>
+ </method>
+
+ </implementation>
+ </binding>
+
+ <binding id="ruleactiontarget-base">
+ <implementation>
+ <constructor>
+ <![CDATA[
+ if (this.parentNode.hasAttribute('initialActionIndex'))
+ {
+ let actionIndex = this.parentNode.getAttribute('initialActionIndex');
+ let filterAction = gFilter.getActionAt(actionIndex);
+ this.parentNode.initWithAction(filterAction);
+ }
+ this.parentNode.updateRemoveButton();
+ ]]>
+ </constructor>
+ </implementation>
+ </binding>
+
+ <binding id="ruleactiontarget-tag" extends="chrome://messenger/content/searchWidgets.xml#ruleactiontarget-base">
+ <content>
+ <xul:menulist class="ruleactionitem">
+ <xul:menupopup>
+ </xul:menupopup>
+ </xul:menulist>
+ </content>
+
+ <implementation>
+ <constructor>
+ <![CDATA[
+ let menuPopup = document.getAnonymousNodes(this)[0].menupopup;
+ let tagArray = MailServices.tags.getAllTags({});
+ for (let i = 0; i < tagArray.length; ++i)
+ {
+ var taginfo = tagArray[i];
+ var newMenuItem = document.createElement('menuitem');
+ newMenuItem.setAttribute('label', taginfo.tag);
+ newMenuItem.setAttribute('value', taginfo.key);
+ menuPopup.appendChild(newMenuItem);
+ }
+ // propagating a pre-existing hack to make the tag get displayed correctly in the menulist
+ // now that we've changed the tags for each menu list. We need to use the current selectedIndex
+ // (if its defined) to handle the case where we were initialized with a filter action already.
+ var currentItem = document.getAnonymousNodes(this)[0].selectedItem;
+ document.getAnonymousNodes(this)[0].selectedItem = null;
+ document.getAnonymousNodes(this)[0].selectedItem = currentItem;
+ ]]>
+ </constructor>
+ </implementation>
+ </binding>
+
+ <binding id="ruleactiontarget-priority" extends="chrome://messenger/content/searchWidgets.xml#ruleactiontarget-base">
+ <content>
+ <xul:menulist class="ruleactionitem">
+ <xul:menupopup>
+ <xul:menuitem value="6" label="&highestPriorityCmd.label;"/>
+ <xul:menuitem value="5" label="&highPriorityCmd.label;"/>
+ <xul:menuitem value="4" label="&normalPriorityCmd.label;"/>
+ <xul:menuitem value="3" label="&lowPriorityCmd.label;"/>
+ <xul:menuitem value="2" label="&lowestPriorityCmd.label;"/>
+ </xul:menupopup>
+ </xul:menulist>
+ </content>
+ </binding>
+
+ <binding id="ruleactiontarget-junkscore" extends="chrome://messenger/content/searchWidgets.xml#ruleactiontarget-base">
+ <content>
+ <xul:menulist class="ruleactionitem">
+ <xul:menupopup>
+ <xul:menuitem value="100" label="&junk.label;"/>
+ <xul:menuitem value="0" label="&notJunk.label;"/>
+ </xul:menupopup>
+ </xul:menulist>
+ </content>
+ </binding>
+
+ <binding id="ruleactiontarget-replyto" extends="chrome://messenger/content/searchWidgets.xml#ruleactiontarget-base">
+ <content>
+ <xul:menulist class="ruleactionitem">
+ <xul:menupopup>
+ </xul:menupopup>
+ </xul:menulist>
+ </content>
+
+ <implementation>
+ <constructor>
+ <![CDATA[
+ document.getAnonymousElementByAttribute(
+ this.parentNode, "class", "ruleactiontype")
+ .getTemplates(true, document.getAnonymousNodes(this)[0]);
+ ]]>
+ </constructor>
+ </implementation>
+ </binding>
+
+ <binding id="ruleactiontarget-forwardto" extends="chrome://messenger/content/searchWidgets.xml#ruleactiontarget-base">
+ <content>
+ <xul:textbox class="ruleactionitem" flex="1"/>
+ </content>
+ </binding>
+
+ <binding id="ruleactiontarget-folder" extends="chrome://messenger/content/searchWidgets.xml#ruleactiontarget-base">
+ <content>
+ <xul:menulist class="ruleactionitem folderMenuItem"
+ displayformat="verbose"
+ oncommand="this.parentNode.setPicker(event);">
+ <xul:menupopup type="folder"
+ mode="filing"
+ class="menulist-menupopup"
+ showRecent="true"
+ recentLabel="&recentFolders.label;"
+ showFileHereLabel="true"/>
+ </xul:menulist>
+ </content>
+
+ <implementation>
+ <constructor>
+ <![CDATA[
+ Components.utils.import("resource:///modules/MailUtils.js", this);
+ let folder = this.menulist.value ?
+ this.MailUtils.getFolderForURI(this.menulist.value) :
+ gFilterList.folder;
+ // An account folder is not a move/copy target; show "Choose Folder".
+ folder = folder.isServer ? null : folder;
+ let menupopup = this.menulist.menupopup;
+ // The menupopup constructor needs to finish first.
+ setTimeout(function() { menupopup.selectFolder(folder); }, 0);
+ ]]>
+ </constructor>
+
+ <field name="menulist">document.getAnonymousNodes(this)[0]</field>
+ <method name="setPicker">
+ <parameter name="aEvent"/>
+ <body>
+ <![CDATA[
+ this.menulist.menupopup.selectFolder(aEvent.target._folder);
+ ]]>
+ </body>
+ </method>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/mailnews/base/search/content/viewLog.js b/mailnews/base/search/content/viewLog.js
new file mode 100644
index 000000000..61ae44df7
--- /dev/null
+++ b/mailnews/base/search/content/viewLog.js
@@ -0,0 +1,36 @@
+/* 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/. */
+
+var gFilterList;
+var gLogFilters;
+var gLogView;
+
+function onLoad()
+{
+ gFilterList = window.arguments[0].filterList;
+
+ gLogFilters = document.getElementById("logFilters");
+ gLogFilters.checked = gFilterList.loggingEnabled;
+
+ gLogView = document.getElementById("logView");
+
+ // for security, disable JS
+ gLogView.docShell.allowJavascript = false;
+
+ gLogView.setAttribute("src", gFilterList.logURL);
+}
+
+function toggleLogFilters()
+{
+ gFilterList.loggingEnabled = gLogFilters.checked;
+}
+
+function clearLog()
+{
+ gFilterList.clearLog();
+
+ // reload the newly truncated file
+ gLogView.reload();
+}
+
diff --git a/mailnews/base/search/content/viewLog.xul b/mailnews/base/search/content/viewLog.xul
new file mode 100644
index 000000000..4ee766395
--- /dev/null
+++ b/mailnews/base/search/content/viewLog.xul
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/viewLog.dtd">
+
+<dialog id="viewLogWindow"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="onLoad();"
+ title="&viewLog.title;"
+ windowtype="mailnews:filterlog"
+ buttons="accept"
+ buttonlabelaccept="&closeLog.label;"
+ buttonaccesskeyaccept="&closeLog.accesskey;"
+ ondialogaccept="window.close();"
+ persist="screenX screenY width height"
+ style="width: 40em; height: 25em;">
+
+ <script type="application/javascript" src="chrome://messenger/content/viewLog.js"/>
+
+ <vbox flex="1">
+ <description>&viewLogInfo.text;</description>
+ <hbox>
+ <checkbox id="logFilters"
+ label="&enableLog.label;"
+ accesskey="&enableLog.accesskey;"
+ oncommand="toggleLogFilters();"/>
+ <spacer flex="1"/>
+ <button label="&clearLog.label;"
+ accesskey="&clearLog.accesskey;"
+ oncommand="clearLog();"/>
+ </hbox>
+ <separator class="thin"/>
+ <hbox flex="1">
+ <browser id="logView"
+ class="inset"
+ type="content"
+ disablehistory="true"
+ disablesecurity="true"
+ src="about:blank"
+ autofind="false"
+ flex="1"/>
+ </hbox>
+ </vbox>
+</dialog>