summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/search/content
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/mailnews/search/content/CustomHeaders.js194
-rw-r--r--comm/mailnews/search/content/CustomHeaders.xhtml61
-rw-r--r--comm/mailnews/search/content/FilterEditor.js809
-rw-r--r--comm/mailnews/search/content/FilterEditor.xhtml136
-rw-r--r--comm/mailnews/search/content/searchTerm.inc.xhtml27
-rw-r--r--comm/mailnews/search/content/searchTerm.js568
-rw-r--r--comm/mailnews/search/content/searchWidgets.js1779
-rw-r--r--comm/mailnews/search/content/viewLog.js38
-rw-r--r--comm/mailnews/search/content/viewLog.xhtml65
9 files changed, 3677 insertions, 0 deletions
diff --git a/comm/mailnews/search/content/CustomHeaders.js b/comm/mailnews/search/content/CustomHeaders.js
new file mode 100644
index 0000000000..4bfc4a3b78
--- /dev/null
+++ b/comm/mailnews/search/content/CustomHeaders.js
@@ -0,0 +1,194 @@
+/* -*- 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/. */
+
+var gAddButton;
+var gRemoveButton;
+var gHeaderInputElement;
+var gArrayHdrs;
+var gHdrsList;
+var gContainer;
+var gFilterBundle = null;
+var gCustomBundle = null;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+document.addEventListener("dialogaccept", onOk);
+document.addEventListener("dialogextra1", onAddHeader);
+document.addEventListener("dialogextra2", onRemoveHeader);
+
+function onLoad() {
+ let hdrs = Services.prefs.getCharPref("mailnews.customHeaders");
+ gHeaderInputElement = document.getElementById("headerInput");
+ gHeaderInputElement.focus();
+
+ gHdrsList = document.getElementById("headerList");
+ gArrayHdrs = [];
+ 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]) {
+ // Remove any null elements.
+ gArrayHdrs.splice(i, 1);
+ }
+ }
+ 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;
+}
+
+function customHeaderOverflow() {
+ var nsMsgSearchAttrib = Ci.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 = listitem.firstElementChild.getAttribute("value").trim();
+ for (let i = 0; i < gArrayHdrs.length; i++) {
+ if (gArrayHdrs[i] == selectedHdr) {
+ gArrayHdrs.splice(i, 1);
+ break;
+ }
+ }
+}
+
+function addRow(newHdr) {
+ return gHdrsList.appendItem(newHdr, "");
+}
+
+function updateAddButton(aDisable) {
+ // only update the button if the disabled state changed
+ if (aDisable == gAddButton.disabled) {
+ return;
+ }
+
+ gAddButton.disabled = aDisable;
+ document.querySelector("dialog").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/comm/mailnews/search/content/CustomHeaders.xhtml b/comm/mailnews/search/content/CustomHeaders.xhtml
new file mode 100644
index 0000000000..9a2ac8468f
--- /dev/null
+++ b/comm/mailnews/search/content/CustomHeaders.xhtml
@@ -0,0 +1,61 @@
+<?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/. -->
+#ifdef MOZ_THUNDERBIRD
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+#else
+<?xml-stylesheet href="chrome://communicator/skin/" type="text/css"?>
+#endif
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/input-fields.css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/CustomHeaders.dtd">
+<html id="customHeaderDialog" xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ width="450" height="375"
+ persist="width height screenX screenY"
+ scrolling="false">
+<head>
+ <title>&window.title;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/CustomHeaders.js"></script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<dialog buttons="accept,cancel,extra1,extra2">
+ <stringbundle id="bundle_filter" src="chrome://messenger/locale/filter.properties"/>
+ <stringbundle id="bundle_custom" src="chrome://messenger/locale/custom.properties"/>
+
+ <hbox flex="1">
+ <vbox flex="1">
+ <label id="headerInputLabel"
+ accesskey="&newMsgHeader.accesskey;"
+ control="headerInput"
+ value="&newMsgHeader.label;"/>
+ <html:input id="headerInput"
+ type="text"
+ aria-labelledby="headerInputLabel"
+ class="input-inline"
+ oninput="onTextInput();"/>
+ <richlistbox id="headerList"
+ class="theme-listbox"
+ flex="1"
+ onselect="updateRemoveButton();" />
+ </vbox>
+ <vbox>
+ <label value=""/>
+ <button id="addButton"
+ label="&addButton.label;"
+ accesskey="&addButton.accesskey;"
+ dlgtype="extra1"/>
+ <button id="removeButton"
+ label="&removeButton.label;"
+ accesskey="&removeButton.accesskey;"
+ dlgtype="extra2"/>
+ </vbox>
+ </hbox>
+</dialog>
+</html:body>
+</html>
diff --git a/comm/mailnews/search/content/FilterEditor.js b/comm/mailnews/search/content/FilterEditor.js
new file mode 100644
index 0000000000..3b93773192
--- /dev/null
+++ b/comm/mailnews/search/content/FilterEditor.js
@@ -0,0 +1,809 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from searchTerm.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+
+// The actual filter that we're editing if it is a _saved_ filter or prefill;
+// void otherwise.
+var gFilter;
+// cache the key elements we need
+var gFilterList;
+// The filter name as it appears in the "Filter Name" field of dialog.
+var gFilterNameElement;
+var gFilterTypeSelector;
+var gFilterBundle;
+var gPreFillName;
+var gFilterActionList;
+var gCustomActions = null;
+var gFilterType;
+var gFilterPosition = 0;
+
+var gFilterActionStrings = [
+ "none",
+ "movemessage",
+ "setpriorityto",
+ "deletemessage",
+ "markasread",
+ "ignorethread",
+ "watchthread",
+ "markasflagged",
+ "label",
+ "replytomessage",
+ "forwardmessage",
+ "stopexecution",
+ "deletefrompopserver",
+ "leaveonpopserver",
+ "setjunkscore",
+ "fetchfrompopserver",
+ "copymessage",
+ "addtagtomessage",
+ "ignoresubthread",
+ "markasunread",
+];
+
+// A temporary filter with the current state of actions in the UI.
+var gTempFilter = null;
+// nsIMsgRuleAction[] - the currently defined actions in the order they will be run.
+var gActionListOrdered = null;
+
+var gFilterEditorMsgWindow = null;
+
+window.addEventListener("DOMContentLoaded", filterEditorOnLoad, { once: true });
+document.addEventListener("dialogaccept", onAccept);
+
+function filterEditorOnLoad() {
+ getCustomActions();
+ initializeSearchWidgets();
+ initializeFilterWidgets();
+
+ gFilterBundle = document.getElementById("bundle_filter");
+
+ if ("arguments" in window && window.arguments[0]) {
+ var args = window.arguments[0];
+
+ if ("filterList" in args) {
+ gFilterList = args.filterList;
+ // the postPlugin filters cannot be applied to servers that are
+ // deferred, (you must define them on the deferredTo server instead).
+ let server = gFilterList.folder.server;
+ if (server.rootFolder != server.rootMsgFolder) {
+ gFilterTypeSelector.disableDeferredAccount();
+ }
+ }
+
+ if ("filterPosition" in args) {
+ gFilterPosition = args.filterPosition;
+ }
+
+ if ("filter" in args) {
+ // editing a filter
+ gFilter = window.arguments[0].filter;
+ initializeDialog(gFilter);
+ } else {
+ if (gFilterList) {
+ setSearchScope(getScopeFromFilterList(gFilterList));
+ }
+ // if doing prefill filter create a new filter and populate it.
+ if ("filterName" in args) {
+ gPreFillName = args.filterName;
+
+ // Passing null as the parameter to createFilter to keep the name empty
+ // until later where we assign the name.
+ gFilter = gFilterList.createFilter(null);
+
+ var term = gFilter.createTerm();
+
+ term.attrib = Ci.nsMsgSearchAttrib.Default;
+ if ("fieldName" in args && args.fieldName) {
+ // fieldName should contain the name of the field in which to search,
+ // from nsMsgSearchTerm.cpp::SearchAttribEntryTable, e.g. "to" or "cc"
+ try {
+ term.attrib = term.getAttributeFromString(args.fieldName);
+ } catch (e) {
+ /* Invalid string is fine, just ignore it. */
+ }
+ }
+ if (term.attrib == Ci.nsMsgSearchAttrib.Default) {
+ term.attrib = Ci.nsMsgSearchAttrib.Sender;
+ }
+
+ term.op = Ci.nsMsgSearchOp.Is;
+ term.booleanAnd = gSearchBooleanRadiogroup.value == "and";
+
+ var termValue = term.value;
+ termValue.attrib = term.attrib;
+ termValue.str = gPreFillName;
+
+ term.value = termValue;
+
+ gFilter.appendTerm(term);
+
+ // the default action for news filters is Delete
+ // for everything else, it's MoveToFolder
+ var filterAction = gFilter.createAction();
+ filterAction.type =
+ getScopeFromFilterList(gFilterList) == Ci.nsMsgSearchScope.newsFilter
+ ? Ci.nsMsgFilterAction.Delete
+ : Ci.nsMsgFilterAction.MoveToFolder;
+ gFilter.appendAction(filterAction);
+ initializeDialog(gFilter);
+ } else if ("copiedFilter" in args) {
+ // we are copying a filter
+ let copiedFilter = args.copiedFilter;
+ let copiedName = gFilterBundle.getFormattedString(
+ "copyToNewFilterName",
+ [copiedFilter.filterName]
+ );
+ let newFilter = gFilterList.createFilter(copiedName);
+
+ // copy the actions
+ for (let i = 0; i < copiedFilter.actionCount; i++) {
+ let filterAction = copiedFilter.getActionAt(i);
+ newFilter.appendAction(filterAction);
+ }
+
+ // copy the search terms
+ for (let searchTerm of copiedFilter.searchTerms) {
+ let newTerm = newFilter.createTerm();
+ newTerm.attrib = searchTerm.attrib;
+ newTerm.op = searchTerm.op;
+ newTerm.booleanAnd = searchTerm.booleanAnd;
+ newTerm.value = searchTerm.value;
+ newFilter.appendTerm(newTerm);
+ }
+
+ newFilter.filterType = copiedFilter.filterType;
+
+ gPreFillName = copiedName;
+ gFilter = newFilter;
+
+ initializeDialog(gFilter);
+
+ // We reset the filter name, because otherwise the saveFilter()
+ // function thinks we are editing a filter, and will thus skip the name
+ // uniqueness check.
+ gFilter.filterName = "";
+ } else {
+ // fake the first more button press
+ onMore(null);
+ }
+ }
+ }
+
+ if (!gFilter) {
+ // This is a new filter. Set to both Incoming and Manual contexts.
+ gFilterTypeSelector.setType(
+ Ci.nsMsgFilterType.Incoming | Ci.nsMsgFilterType.Manual
+ );
+ }
+
+ // in the case of a new filter, we may not have an action row yet.
+ ensureActionRow();
+ gFilterType = gFilterTypeSelector.getType();
+
+ gFilterNameElement.select();
+ // This call is required on mac and linux. It has no effect under win32. See bug 94800.
+ gFilterNameElement.focus();
+}
+
+function onEnterInSearchTerm(event) {
+ if (event.ctrlKey || (Services.appinfo.OS == "Darwin" && event.metaKey)) {
+ // If accel key (Ctrl on Win/Linux, Cmd on Mac) was held too, accept the dialog.
+ document.querySelector("dialog").acceptDialog();
+ } else {
+ // If only plain Enter was pressed, add a new rule line.
+ onMore(event);
+ }
+}
+
+function onAccept(event) {
+ try {
+ if (!saveFilter()) {
+ event.preventDefault();
+ return;
+ }
+ } catch (e) {
+ console.error(e);
+ event.preventDefault();
+ return;
+ }
+
+ // parent should refresh filter list..
+ // this should REALLY only happen when some criteria changes that
+ // are displayed in the filter dialog, like the filter name
+ window.arguments[0].refresh = true;
+ window.arguments[0].newFilter = gFilter;
+}
+
+function duplicateFilterNameExists(filterName) {
+ if (gFilterList) {
+ for (var i = 0; i < gFilterList.filterCount; i++) {
+ if (filterName == gFilterList.getFilterAt(i).filterName) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+function getScopeFromFilterList(filterList) {
+ if (!filterList) {
+ dump("yikes, null filterList\n");
+ return Ci.nsMsgSearchScope.offlineMail;
+ }
+ return filterList.folder.server.filterScope;
+}
+
+function getScope(filter) {
+ return getScopeFromFilterList(filter.filterList);
+}
+
+function initializeFilterWidgets() {
+ gFilterNameElement = document.getElementById("filterName");
+ gFilterActionList = document.getElementById("filterActionList");
+ initializeFilterTypeSelector();
+}
+
+function initializeFilterTypeSelector() {
+ /**
+ * This object controls code interaction with the widget allowing specifying
+ * the filter type (event when the filter is run).
+ */
+ gFilterTypeSelector = {
+ checkBoxManual: document.getElementById("runManual"),
+ checkBoxIncoming: document.getElementById("runIncoming"),
+
+ menulistIncoming: document.getElementById("pluginsRunOrder"),
+
+ menuitemBeforePlugins: document.getElementById("runBeforePlugins"),
+ menuitemAfterPlugins: document.getElementById("runAfterPlugins"),
+
+ checkBoxArchive: document.getElementById("runArchive"),
+ checkBoxOutgoing: document.getElementById("runOutgoing"),
+ checkBoxPeriodic: document.getElementById("runPeriodic"),
+
+ /**
+ * Returns the currently set filter type (checkboxes) in terms
+ * of a Ci.Ci.nsMsgFilterType value.
+ */
+ getType() {
+ let type = Ci.nsMsgFilterType.None;
+
+ if (this.checkBoxManual.checked) {
+ type |= Ci.nsMsgFilterType.Manual;
+ }
+
+ if (this.checkBoxIncoming.checked) {
+ if (this.menulistIncoming.selectedItem == this.menuitemAfterPlugins) {
+ type |= Ci.nsMsgFilterType.PostPlugin;
+ } else if (
+ getScopeFromFilterList(gFilterList) == Ci.nsMsgSearchScope.newsFilter
+ ) {
+ type |= Ci.nsMsgFilterType.NewsRule;
+ } else {
+ type |= Ci.nsMsgFilterType.InboxRule;
+ }
+ }
+
+ if (this.checkBoxArchive.checked) {
+ type |= Ci.nsMsgFilterType.Archive;
+ }
+
+ if (this.checkBoxOutgoing.checked) {
+ type |= Ci.nsMsgFilterType.PostOutgoing;
+ }
+
+ if (this.checkBoxPeriodic.checked) {
+ type |= Ci.nsMsgFilterType.Periodic;
+ }
+
+ return type;
+ },
+
+ /**
+ * Sets the checkboxes to represent the filter type passed in.
+ *
+ * @param aType the filter type to set in terms
+ * of Ci.Ci.nsMsgFilterType values.
+ */
+ setType(aType) {
+ // If there is no type (event) requested, force "when manually run"
+ if (aType == Ci.nsMsgFilterType.None) {
+ aType = Ci.nsMsgFilterType.Manual;
+ }
+
+ this.checkBoxManual.checked = aType & Ci.nsMsgFilterType.Manual;
+
+ this.checkBoxIncoming.checked =
+ aType & (Ci.nsMsgFilterType.PostPlugin | Ci.nsMsgFilterType.Incoming);
+
+ this.menulistIncoming.selectedItem =
+ aType & Ci.nsMsgFilterType.PostPlugin
+ ? this.menuitemAfterPlugins
+ : this.menuitemBeforePlugins;
+
+ this.checkBoxArchive.checked = aType & Ci.nsMsgFilterType.Archive;
+
+ this.checkBoxOutgoing.checked = aType & Ci.nsMsgFilterType.PostOutgoing;
+
+ this.checkBoxPeriodic.checked = aType & Ci.nsMsgFilterType.Periodic;
+ const periodMinutes = gFilterList.folder.server.getIntValue(
+ "periodicFilterRateMinutes"
+ );
+ document.getElementById("runPeriodic").label = PluralForm.get(
+ periodMinutes,
+ gFilterBundle.getString("contextPeriodic.label")
+ ).replace("#1", periodMinutes);
+
+ this.updateClassificationMenu();
+ },
+
+ /**
+ * Enable the "before/after classification" menulist depending on
+ * whether "run when incoming mail" is selected.
+ */
+ updateClassificationMenu() {
+ this.menulistIncoming.disabled = !this.checkBoxIncoming.checked;
+ updateFilterType();
+ },
+
+ /**
+ * Disable the options unsuitable for deferred accounts.
+ */
+ disableDeferredAccount() {
+ this.menuitemAfterPlugins.disabled = true;
+ this.checkBoxOutgoing.disabled = true;
+ },
+ };
+}
+
+function initializeDialog(filter) {
+ gFilterNameElement.value = filter.filterName;
+ gFilterTypeSelector.setType(filter.filterType);
+
+ let numActions = filter.actionCount;
+ for (let actionIndex = 0; actionIndex < numActions; actionIndex++) {
+ let filterAction = filter.getActionAt(actionIndex);
+
+ let newActionRow = document.createXULElement("richlistitem", {
+ is: "ruleaction-richlistitem",
+ });
+ newActionRow.setAttribute("initialActionIndex", actionIndex);
+ newActionRow.className = "ruleaction";
+ gFilterActionList.appendChild(newActionRow);
+ newActionRow.setAttribute(
+ "value",
+ filterAction.type == Ci.nsMsgFilterAction.Custom
+ ? filterAction.customId
+ : gFilterActionStrings[filterAction.type]
+ );
+ newActionRow.setAttribute("onfocus", "this.storeFocus();");
+ }
+
+ var gSearchScope = getFilterScope(
+ getScope(filter),
+ filter.filterType,
+ filter.filterList
+ );
+ initializeSearchRows(gSearchScope, filter.searchTerms);
+ setFilterScope(filter.filterType, filter.filterList);
+}
+
+function ensureActionRow() {
+ // make sure we have at least one action row visible to the user
+ if (!gFilterActionList.getRowCount()) {
+ let newActionRow = document.createXULElement("richlistitem", {
+ is: "ruleaction-richlistitem",
+ });
+ newActionRow.className = "ruleaction";
+ gFilterActionList.appendChild(newActionRow);
+ newActionRow.mRemoveButton.disabled = true;
+ }
+}
+
+// move to overlay
+function saveFilter() {
+ // See if at least one filter type (activation event) is selected.
+ if (gFilterType == Ci.nsMsgFilterType.None) {
+ Services.prompt.alert(
+ window,
+ gFilterBundle.getString("mustHaveFilterTypeTitle"),
+ gFilterBundle.getString("mustHaveFilterTypeMessage")
+ );
+ return false;
+ }
+
+ let filterName = gFilterNameElement.value;
+ // If we think have a duplicate, then we need to check that if we
+ // have an original filter name (i.e. we are editing a filter), then
+ // we must check that the original is not the current as that is what
+ // the duplicateFilterNameExists function will have picked up.
+ if (
+ (!gFilter || gFilter.filterName != filterName) &&
+ duplicateFilterNameExists(filterName)
+ ) {
+ Services.prompt.alert(
+ window,
+ gFilterBundle.getString("cannotHaveDuplicateFilterTitle"),
+ gFilterBundle.getString("cannotHaveDuplicateFilterMessage")
+ );
+ return false;
+ }
+
+ // Check that all of the search attributes and operators are valid.
+ function rule_desc(index, obj) {
+ return (
+ index +
+ 1 +
+ " (" +
+ obj.searchattribute.label +
+ ", " +
+ obj.searchoperator.label +
+ ")"
+ );
+ }
+
+ let invalidRule = false;
+ for (let index = 0; index < gSearchTerms.length; index++) {
+ let obj = gSearchTerms[index].obj;
+ // We don't need to check validity of matchAll terms
+ if (obj.matchAll) {
+ continue;
+ }
+
+ // the term might be an offscreen one that we haven't initialized yet
+ let searchTerm = obj.searchTerm;
+ if (!searchTerm && !gSearchTerms[index].initialized) {
+ continue;
+ }
+
+ if (isNaN(obj.searchattribute.value)) {
+ // is this a custom term?
+ let customTerm = MailServices.filters.getCustomTerm(
+ obj.searchattribute.value
+ );
+ if (!customTerm) {
+ invalidRule = true;
+ console.error(
+ "Filter not saved because custom search term '" +
+ obj.searchattribute.value +
+ "' in rule " +
+ rule_desc(index, obj) +
+ " not found"
+ );
+ } else if (
+ !customTerm.getAvailable(obj.searchScope, obj.searchattribute.value)
+ ) {
+ invalidRule = true;
+ console.error(
+ "Filter not saved because custom search term '" +
+ customTerm.name +
+ "' in rule " +
+ rule_desc(index, obj) +
+ " not available"
+ );
+ }
+ } else {
+ let otherHeader = Ci.nsMsgSearchAttrib.OtherHeader;
+ let attribValue =
+ obj.searchattribute.value > otherHeader
+ ? otherHeader
+ : obj.searchattribute.value;
+ if (
+ !obj.searchattribute.validityTable.getAvailable(
+ attribValue,
+ obj.searchoperator.value
+ )
+ ) {
+ invalidRule = true;
+ console.error(
+ "Filter not saved because standard search term '" +
+ attribValue +
+ "' in rule " +
+ rule_desc(index, obj) +
+ " not available in this context"
+ );
+ }
+ }
+
+ if (invalidRule) {
+ Services.prompt.alert(
+ window,
+ gFilterBundle.getString("searchTermsInvalidTitle"),
+ gFilterBundle.getFormattedString("searchTermsInvalidRule", [
+ obj.searchattribute.label,
+ obj.searchoperator.label,
+ ])
+ );
+ return false;
+ }
+ }
+
+ // before we go any further, validate each specified filter action, abort the save
+ // if any of the actions is invalid...
+ for (let index = 0; index < gFilterActionList.itemCount; index++) {
+ var listItem = gFilterActionList.getItemAtIndex(index);
+ if (!listItem.validateAction()) {
+ return false;
+ }
+ }
+
+ // if we made it here, all of the actions are valid, so go ahead and save the filter
+ let isNewFilter;
+ if (!gFilter) {
+ // This is a new filter
+ gFilter = gFilterList.createFilter(filterName);
+ isNewFilter = true;
+ gFilter.enabled = true;
+ } else {
+ // We are working with an existing filter object,
+ // either editing or using prefill
+ gFilter.filterName = filterName;
+ // Prefilter is treated as a new filter.
+ if (gPreFillName) {
+ isNewFilter = true;
+ gFilter.enabled = true;
+ } else {
+ isNewFilter = false;
+ }
+
+ gFilter.clearActionList();
+ }
+
+ // add each filteraction to the filter
+ for (let index = 0; index < gFilterActionList.itemCount; index++) {
+ gFilterActionList.getItemAtIndex(index).saveToFilter(gFilter);
+ }
+
+ // If we do not have a filter name at this point, generate one.
+ if (!gFilter.filterName) {
+ AssignMeaningfulName();
+ }
+
+ gFilter.filterType = gFilterType;
+ gFilter.searchTerms = saveSearchTerms(gFilter.searchTerms, gFilter);
+
+ if (isNewFilter) {
+ // new filter - insert into gFilterList
+ gFilterList.insertFilterAt(gFilterPosition, gFilter);
+ }
+
+ // success!
+ return true;
+}
+
+/**
+ * Check if the list of actions the user created will be executed in a different order.
+ * Exposes a note to the user if that is the case.
+ */
+function checkActionsReorder() {
+ setTimeout(_checkActionsReorder, 0);
+}
+
+/**
+ * This should be called from setTimeout otherwise some of the elements calling
+ * may not be fully initialized yet (e.g. we get ".saveToFilter is not a function").
+ * It is OK to schedule multiple timeouts with this function.
+ */
+function _checkActionsReorder() {
+ // Create a temporary disposable filter and add current actions to it.
+ if (!gTempFilter) {
+ gTempFilter = gFilterList.createFilter("");
+ } else {
+ gTempFilter.clearActionList();
+ }
+
+ for (let index = 0; index < gFilterActionList.itemCount; index++) {
+ gFilterActionList.getItemAtIndex(index).saveToFilter(gTempFilter);
+ }
+
+ // Now get the actions out of the filter in the order they will be executed in.
+ gActionListOrdered = gTempFilter.sortedActionList;
+
+ // Compare the two lists.
+ let statusBar = document.getElementById("statusbar");
+ for (let index = 0; index < gActionListOrdered.length; index++) {
+ if (index != gTempFilter.getActionIndex(gActionListOrdered[index])) {
+ // If the lists are not the same unhide the status bar and show warning.
+ statusBar.style.visibility = "visible";
+ return;
+ }
+ }
+
+ statusBar.style.visibility = "hidden";
+}
+
+/**
+ * Show a dialog with the ordered list of actions.
+ * The fetching of action label and argument is separated from checkActionsReorder
+ * function to make that one more lightweight. The list is built only upon
+ * user request.
+ */
+function showActionsOrder() {
+ // Fetch the actions and arguments as a string.
+ let actionStrings = [];
+ for (let i = 0; i < gFilterActionList.itemCount; i++) {
+ let ruleAction = gFilterActionList.getItemAtIndex(i);
+ let actionTarget = ruleAction.children[1];
+ let actionItem = actionTarget.ruleactiontargetElement;
+ let actionItemLabel = actionItem && actionItem.children[0].label;
+
+ let actionString = {
+ label: ruleAction.mRuleActionType.label,
+ argument: "",
+ };
+ if (actionItem) {
+ if (actionItemLabel) {
+ actionString.argument = actionItemLabel;
+ } else {
+ actionString.argument = actionItem.children[0].value;
+ }
+ }
+ actionStrings.push(actionString);
+ }
+
+ // Present a nicely formatted list of action names and arguments.
+ let actionList = gFilterBundle.getString("filterActionOrderExplanation");
+ for (let i = 0; i < gActionListOrdered.length; i++) {
+ let actionIndex = gTempFilter.getActionIndex(gActionListOrdered[i]);
+ let action = actionStrings[actionIndex];
+ actionList += gFilterBundle.getFormattedString("filterActionItem", [
+ i + 1,
+ action.label,
+ action.argument,
+ ]);
+ }
+
+ Services.prompt.confirmEx(
+ window,
+ gFilterBundle.getString("filterActionOrderTitle"),
+ actionList,
+ Services.prompt.BUTTON_TITLE_OK,
+ null,
+ null,
+ null,
+ null,
+ { value: false }
+ );
+}
+
+function AssignMeaningfulName() {
+ // termRoot points to the first search object, which is the one we care about.
+ let termRoot = gSearchTerms[0].obj;
+ // stub is used as the base name for a filter.
+ let stub;
+
+ // If this is a Match All Messages Filter, we already know the name to assign.
+ if (termRoot.matchAll) {
+ stub = gFilterBundle.getString("matchAllFilterName");
+ } else {
+ // Assign a name based on the first search term.
+ let term = termRoot.searchattribute.label;
+ let operator = termRoot.searchoperator.label;
+ let value = termRoot.searchvalue.getReadableValue();
+ stub = gFilterBundle.getFormattedString("filterAutoNameStr", [
+ term,
+ operator,
+ value,
+ ]);
+ }
+
+ // Whatever name we have used, 'uniquify' it.
+ let tempName = stub;
+ let count = 1;
+ while (duplicateFilterNameExists(tempName)) {
+ count++;
+ tempName = `${stub} ${count}`;
+ }
+ gFilter.filterName = tempName;
+}
+
+function UpdateAfterCustomHeaderChange() {
+ updateSearchAttributes();
+}
+
+// if you use msgWindow, please make sure that destructor gets called when you close the "window"
+function GetFilterEditorMsgWindow() {
+ if (!gFilterEditorMsgWindow) {
+ var msgWindowContractID = "@mozilla.org/messenger/msgwindow;1";
+ var nsIMsgWindow = Ci.nsIMsgWindow;
+ gFilterEditorMsgWindow =
+ Cc[msgWindowContractID].createInstance(nsIMsgWindow);
+ gFilterEditorMsgWindow.domWindow = window;
+ gFilterEditorMsgWindow.rootDocShell.appType = Ci.nsIDocShell.APP_TYPE_MAIL;
+ }
+ return gFilterEditorMsgWindow;
+}
+
+function SetBusyCursor(window, enable) {
+ // setCursor() is only available for chrome windows.
+ // However one of our frames is the start page which
+ // is a non-chrome window, so check if this window has a
+ // setCursor method
+ if ("setCursor" in window) {
+ if (enable) {
+ window.setCursor("wait");
+ } else {
+ window.setCursor("auto");
+ }
+ }
+}
+
+/* globals openHelp */
+// suite/components/helpviewer/content/contextHelp.js
+function doHelpButton() {
+ openHelp("mail-filters");
+}
+
+function getCustomActions() {
+ if (!gCustomActions) {
+ gCustomActions = MailServices.filters.getCustomActions();
+ }
+}
+
+function updateFilterType() {
+ gFilterType = gFilterTypeSelector.getType();
+ setFilterScope(gFilterType, gFilterList);
+
+ // set valid actions
+ var ruleActions = gFilterActionList.getElementsByAttribute(
+ "class",
+ "ruleaction"
+ );
+ for (var i = 0; i < ruleActions.length; i++) {
+ ruleActions[i].mRuleActionType.hideInvalidActions();
+ }
+}
+
+// Given a filter type, set the global search scope to the filter scope
+function setFilterScope(aFilterType, aFilterList) {
+ let filterScope = getFilterScope(
+ getScopeFromFilterList(aFilterList),
+ aFilterType,
+ aFilterList
+ );
+ setSearchScope(filterScope);
+}
+
+//
+// Given the base filter scope for a server, and the filter
+// type, return the scope used for filter. This assumes a
+// hierarchy of contexts, with incoming the most restrictive,
+// followed by manual and post-plugin.
+function getFilterScope(aServerFilterScope, aFilterType, aFilterList) {
+ if (aFilterType & Ci.nsMsgFilterType.Incoming) {
+ return aServerFilterScope;
+ }
+
+ // Manual or PostPlugin
+ // local mail allows body and junk types
+ if (aServerFilterScope == Ci.nsMsgSearchScope.offlineMailFilter) {
+ return Ci.nsMsgSearchScope.offlineMail;
+ }
+ // IMAP and NEWS online don't allow body
+ return Ci.nsMsgSearchScope.onlineManual;
+}
+
+/**
+ * Re-focus the action that was focused before focus was lost.
+ */
+function setLastActionFocus() {
+ let lastAction = gFilterActionList.getAttribute("focusedAction");
+ if (!lastAction || lastAction < 0) {
+ lastAction = 0;
+ }
+ if (lastAction >= gFilterActionList.itemCount) {
+ lastAction = gFilterActionList.itemCount - 1;
+ }
+
+ gFilterActionList.getItemAtIndex(lastAction).mRuleActionType.focus();
+}
diff --git a/comm/mailnews/search/content/FilterEditor.xhtml b/comm/mailnews/search/content/FilterEditor.xhtml
new file mode 100644
index 0000000000..00188392bc
--- /dev/null
+++ b/comm/mailnews/search/content/FilterEditor.xhtml
@@ -0,0 +1,136 @@
+<?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"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/filterDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/filterEditor.css" type="text/css"?>
+
+<!DOCTYPE html [
+ <!ENTITY % filterEditorDTD SYSTEM "chrome://messenger/locale/FilterEditor.dtd">
+ %filterEditorDTD;
+ <!ENTITY % searchTermDTD SYSTEM "chrome://messenger/locale/searchTermOverlay.dtd">
+ %searchTermDTD;
+]>
+<html id="FilterEditor" xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="mailnews:filtereditor"
+ lightweightthemes="true"
+ style="min-width: 900px; min-height: 600px;"
+ scrolling="false">
+<head>
+ <title>&window.title;</title>
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/searchWidgets.js"></script>
+ <script defer="defer" src="chrome://messenger/content/mailWindowOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/mailCommands.js"></script>
+ <script defer="defer" src="chrome://messenger/content/searchTerm.js"></script>
+ <script defer="defer" src="chrome://messenger/content/dateFormat.js"></script>
+ <script defer="defer" src="chrome://messenger/content/dialogShadowDom.js"></script>
+ <script defer="defer" src="chrome://messenger/content/FilterEditor.js"></script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<dialog buttons="accept,cancel">
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+ <stringbundle id="bundle_filter" src="chrome://messenger/locale/filter.properties"/>
+
+ <commandset>
+ <command id="cmd_updateFilterType" oncommand="updateFilterType();"/>
+ <command id="cmd_updateClassificationMenu" oncommand="gFilterTypeSelector.updateClassificationMenu();"/>
+ </commandset>
+
+ <html:div id="filterNameBox" class="input-container">
+ <label id="filterNameLabel"
+ value="&filterName.label;"
+ accesskey="&filterName.accesskey;"
+ control="filterName"/>
+ <html:input id="filterName"
+ type="text"
+ class="input-inline"
+ aria-labelledby="filterNameLabel"/>
+ </html:div>
+
+ <html:fieldset id="applyFiltersSettings">
+ <html:legend>&contextDesc.label;</html:legend>
+ <vbox>
+ <hbox flex="1" align="center">
+ <checkbox id="runManual"
+ label="&contextManual.label;"
+ accesskey="&contextManual.accesskey;"
+ command="cmd_updateFilterType"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <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>
+ </hbox>
+ <hbox flex="1" align="center">
+ <checkbox id="runArchive"
+ label="&contextArchive.label;"
+ accesskey="&contextArchive.accesskey;"
+ command="cmd_updateFilterType"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <checkbox id="runOutgoing"
+ label="&contextOutgoing.label;"
+ accesskey="&contextOutgoing.accesskey;"
+ command="cmd_updateFilterType"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <checkbox id="runPeriodic"
+ accesskey="&contextPeriodic.accesskey;"
+ command="cmd_updateFilterType"/>
+ <label id="periodLength"/>
+ </hbox>
+ </vbox>
+ </html:fieldset>
+
+
+ <vbox id="searchTermListBox">
+#include searchTerm.inc.xhtml
+
+ <splitter id="gray_horizontal_splitter" persist="state" orient="vertical"/>
+
+ <vbox id="filterActionsBox">
+ <label value="&filterActionDesc.label;"
+ accesskey="&filterActionDesc.accesskey;"
+ control="filterActionList"/>
+ <richlistbox id="filterActionList"
+ flex="1"
+ style="min-height: 100px;"
+ onfocus="setLastActionFocus();"
+ focusedAction="0">
+ </richlistbox>
+ </vbox>
+
+ <vbox id="statusbar" style="visibility: hidden;">
+ <hbox align="center">
+ <label>
+ &filterActionOrderWarning.label;
+ </label>
+ <label id="seeExecutionOrder" class="text-link"
+ onclick="showActionsOrder();">&filterActionOrder.label;</label>
+ </hbox>
+ </vbox>
+</dialog>
+</html:body>
+</html>
diff --git a/comm/mailnews/search/content/searchTerm.inc.xhtml b/comm/mailnews/search/content/searchTerm.inc.xhtml
new file mode 100644
index 0000000000..07e3407a4c
--- /dev/null
+++ b/comm/mailnews/search/content/searchTerm.inc.xhtml
@@ -0,0 +1,27 @@
+# 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/.
+
+ <radiogroup id="booleanAndGroup" orient="horizontal" value="and"
+ oncommand="booleanChanged(event);">
+ <radio value="and" label="&matchAll.label;"
+ accesskey="&matchAll.accesskey;" flex="1"/>
+ <radio value="or" label="&matchAny.label;"
+ accesskey="&matchAny.accesskey;" flex="1"/>
+ <radio value="matchAll" id="matchAllItem" label="&matchAllMsgs.label;"
+ accesskey="&matchAllMsgs.accesskey;" flex="1"/>
+ </radiogroup>
+
+ <hbox id="searchTermBox" style="flex: 1 1 0;">
+ <hbox id="searchterms" class="themeable-brighttext"/>
+ <richlistbox id="searchTermList" flex="1">
+ <treecols hidden="true">
+ <treecol style="flex: &searchTermListAttributesFlexValue; auto"/>
+ <treecol style="flex: &searchTermListOperatorsFlexValue; auto"/>
+ <treecol style="flex: &searchTermListValueFlexValue; auto"/>
+ <treecol class="filler"/>
+ </treecols>
+ </richlistbox>
+
+ </hbox>
+ </vbox>
diff --git a/comm/mailnews/search/content/searchTerm.js b/comm/mailnews/search/content/searchTerm.js
new file mode 100644
index 0000000000..10d085a5e0
--- /dev/null
+++ b/comm/mailnews/search/content/searchTerm.js
@@ -0,0 +1,568 @@
+/* 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/. */
+
+// abSearchDialog.js
+/* globals GetScopeForDirectoryURI */
+
+var gTotalSearchTerms = 0;
+var gSearchTermList;
+var gSearchTerms = [];
+var gSearchRemovedTerms = [];
+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 == Ci.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;
+ },
+
+ // 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;
+ }
+ },
+
+ saveId(element, slot) {
+ this[slot] = element.id;
+ },
+
+ getElement(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");
+ },
+ get searchoperator() {
+ return this.getElement("internalSearchOperatorId");
+ },
+ set searchoperator(val) {
+ this.saveId(val, "internalSearchOperatorId");
+ },
+ get searchvalue() {
+ return this.getElement("internalSearchValueId");
+ },
+ set searchvalue(val) {
+ this.saveId(val, "internalSearchValueId");
+ },
+
+ booleanNodes: null,
+ get booleanAnd() {
+ return this.internalBooleanAnd;
+ },
+ set booleanAnd(val) {
+ this.internalBooleanAnd = val;
+ },
+
+ save() {
+ var searchTerm = this.searchTerm;
+ var nsMsgSearchAttrib = Ci.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(searchTerm) {
+ this.internalSearchTerm = searchTerm;
+ this.save();
+ },
+};
+
+function initializeSearchWidgets() {
+ gSearchBooleanRadiogroup = document.getElementById("booleanAndGroup");
+ gSearchTermList = document.getElementById("searchTermList");
+
+ // initialize some strings
+ var bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/search.properties"
+ );
+ gMoreButtonTooltipText = bundle.GetStringFromName("moreButtonTooltipText");
+ gLessButtonTooltipText = bundle.GetStringFromName("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"
+ if (matchAll) {
+ gSearchBooleanRadiogroup.value = "matchAll";
+ } else if (booleanAnd) {
+ gSearchBooleanRadiogroup.value = "and";
+ } else {
+ gSearchBooleanRadiogroup.value = "or";
+ }
+ var searchTerms = document.getElementById("searchTermList");
+ if (searchTerms) {
+ updateSearchTermsListbox(matchAll);
+ }
+}
+
+function initializeSearchRows(scope, searchTerms) {
+ for (let i = 0; i < searchTerms.length; i++) {
+ let searchTerm = searchTerms[i];
+ 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("search-attribute");
+ var searchOperatorList = searchTerms.getElementsByTagName("search-operator");
+ var searchValueList = searchTerms.getElementsByTagName("search-value");
+ for (let 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 (let 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.lastElementChild.lastElementChild.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 != "richlistitem") {
+ 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) {
+ // Fake to get empty row.
+ onMore(null);
+ }
+ 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.createXULElement("search-attribute");
+ var searchOp = document.createXULElement("search-operator");
+ var searchVal = document.createXULElement("search-value");
+
+ var moreButton = document.createXULElement("button");
+ var lessButton = document.createXULElement("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,
+ searchTerm,
+ initialized: false,
+ });
+
+ var editFilter = window.gFilter || null;
+ var editMailView = window.gMailView || null;
+
+ 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 <richlistitem> 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 cols = gSearchTermList.firstElementChild.children; // treecol elements
+ let listitem = document.createXULElement("richlistitem");
+ listitem.setAttribute("allowevents", "true");
+ for (let i = 0; i < aChildren.length; i++) {
+ let listcell = document.createXULElement("hbox");
+ if (cols[i].hasAttribute("style")) {
+ listcell.setAttribute("style", cols[i].getAttribute("style"));
+ }
+ 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 == "richlistitem") {
+ break;
+ }
+ listitem = listitem.parentNode;
+ }
+
+ if (!listitem) {
+ dump("Error: couldn't find parent listitem!\n");
+ return;
+ }
+
+ if (searchTermObj.searchTerm) {
+ gSearchRemovedTerms.push(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.
+ *
+ * @param {nsIMsgSearchTerm[]} searchTerms - Array of terms
+ * @param {object} termOwner - Object which can contain and create the terms
+ * e.g. a nsIMsgSearchSession (will be unnecessary if we just make terms
+ * creatable via XPCOM).
+ * @returns {nsIMsgSearchTerm[]} The filtered searchTerms.
+ */
+function saveSearchTerms(searchTerms, termOwner) {
+ var matchAll = gSearchBooleanRadiogroup.value == "matchAll";
+ var i;
+
+ searchTerms = searchTerms.filter(t => !gSearchRemovedTerms.includes(t));
+
+ 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[i] = searchTerm;
+ } catch (ex) {
+ dump("** Error saving element " + i + ": " + ex + "\n");
+ }
+ }
+ return searchTerms;
+}
+
+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/comm/mailnews/search/content/searchWidgets.js b/comm/mailnews/search/content/searchWidgets.js
new file mode 100644
index 0000000000..add3ed29b8
--- /dev/null
+++ b/comm/mailnews/search/content/searchWidgets.js
@@ -0,0 +1,1779 @@
+/**
+ * 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/. */
+
+/* global MozElements MozXULElement */
+
+/* import-globals-from ../../base/content/dateFormat.js */
+// TODO: This is completely bogus. Only one use of this file also has FilterEditor.js.
+/* import-globals-from FilterEditor.js */
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+ );
+ const { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+ const updateParentNode = parentNode => {
+ if (parentNode.hasAttribute("initialActionIndex")) {
+ let actionIndex = parentNode.getAttribute("initialActionIndex");
+ let filterAction = gFilter.getActionAt(actionIndex);
+ parentNode.initWithAction(filterAction);
+ }
+ parentNode.updateRemoveButton();
+ };
+
+ class MozRuleactiontargetTag extends MozXULElement {
+ connectedCallback() {
+ const menulist = document.createXULElement("menulist");
+ const menuPopup = document.createXULElement("menupopup");
+
+ menulist.classList.add("ruleactionitem");
+ menulist.setAttribute("flex", "1");
+ menulist.appendChild(menuPopup);
+
+ for (let taginfo of MailServices.tags.getAllTags()) {
+ const newMenuItem = document.createXULElement("menuitem");
+ newMenuItem.setAttribute("label", taginfo.tag);
+ newMenuItem.setAttribute("value", taginfo.key);
+ if (taginfo.color) {
+ newMenuItem.setAttribute("style", `color: ${taginfo.color};`);
+ }
+ menuPopup.appendChild(newMenuItem);
+ }
+
+ this.appendChild(menulist);
+
+ updateParentNode(this.closest(".ruleaction"));
+ }
+ }
+
+ class MozRuleactiontargetPriority extends MozXULElement {
+ connectedCallback() {
+ this.appendChild(
+ MozXULElement.parseXULToFragment(
+ `
+ <menulist class="ruleactionitem" flex="1">
+ <menupopup>
+ <menuitem value="6" label="&highestPriorityCmd.label;"></menuitem>
+ <menuitem value="5" label="&highPriorityCmd.label;"></menuitem>
+ <menuitem value="4" label="&normalPriorityCmd.label;"></menuitem>
+ <menuitem value="3" label="&lowPriorityCmd.label;"></menuitem>
+ <menuitem value="2" label="&lowestPriorityCmd.label;"></menuitem>
+ </menupopup>
+ </menulist>
+ `,
+ ["chrome://messenger/locale/FilterEditor.dtd"]
+ )
+ );
+
+ updateParentNode(this.closest(".ruleaction"));
+ }
+ }
+
+ class MozRuleactiontargetJunkscore extends MozXULElement {
+ connectedCallback() {
+ this.appendChild(
+ MozXULElement.parseXULToFragment(
+ `
+ <menulist class="ruleactionitem" flex="1">
+ <menupopup>
+ <menuitem value="100" label="&junk.label;"/>
+ <menuitem value="0" label="&notJunk.label;"/>
+ </menupopup>
+ </menulist>
+ `,
+ ["chrome://messenger/locale/FilterEditor.dtd"]
+ )
+ );
+
+ updateParentNode(this.closest(".ruleaction"));
+ }
+ }
+
+ class MozRuleactiontargetReplyto extends MozXULElement {
+ connectedCallback() {
+ const menulist = document.createXULElement("menulist");
+ const menuPopup = document.createXULElement("menupopup");
+
+ menulist.classList.add("ruleactionitem");
+ menulist.setAttribute("flex", "1");
+ menulist.appendChild(menuPopup);
+
+ this.appendChild(menulist);
+
+ let ruleaction = this.closest(".ruleaction");
+ let raMenulist = ruleaction.querySelector(
+ '[is="ruleactiontype-menulist"]'
+ );
+ for (let { label, value } of raMenulist.findTemplates()) {
+ menulist.appendItem(label, value);
+ }
+ updateParentNode(ruleaction);
+ }
+ }
+
+ class MozRuleactiontargetForwardto extends MozXULElement {
+ connectedCallback() {
+ const input = document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "input"
+ );
+ input.classList.add("ruleactionitem", "input-inline");
+
+ this.classList.add("input-container");
+ this.appendChild(input);
+
+ updateParentNode(this.closest(".ruleaction"));
+ }
+ }
+
+ class MozRuleactiontargetFolder extends MozXULElement {
+ connectedCallback() {
+ this.appendChild(
+ MozXULElement.parseXULToFragment(
+ `
+ <menulist class="ruleactionitem
+ folderMenuItem"
+ flex="1"
+ displayformat="verbose">
+ <menupopup is="folder-menupopup"
+ mode="filing"
+ class="menulist-menupopup"
+ showRecent="true"
+ recentLabel="&recentFolders.label;"
+ showFileHereLabel="true">
+ </menupopup>
+ </menulist>
+ `,
+ ["chrome://messenger/locale/messenger.dtd"]
+ )
+ );
+
+ this.menulist = this.querySelector("menulist");
+
+ this.menulist.addEventListener("command", event => {
+ this.setPicker(event);
+ });
+
+ updateParentNode(this.closest(".ruleaction"));
+
+ let folder = this.menulist.value
+ ? MailUtils.getOrCreateFolder(this.menulist.value)
+ : gFilterList.folder;
+
+ // An account folder is not a move/copy target; show "Choose Folder".
+ folder = folder.isServer ? null : folder;
+
+ this.menulist.menupopup.selectFolder(folder);
+ }
+
+ setPicker(event) {
+ this.menulist.menupopup.selectFolder(event.target._folder);
+ }
+ }
+
+ class MozRuleactiontargetWrapper extends MozXULElement {
+ static get observedAttributes() {
+ return ["type"];
+ }
+
+ get ruleactiontargetElement() {
+ return this.node;
+ }
+
+ connectedCallback() {
+ this._updateAttributes();
+ }
+
+ attributeChangedCallback() {
+ this._updateAttributes();
+ }
+
+ _getChildNode(type) {
+ const elementMapping = {
+ movemessage: "ruleactiontarget-folder",
+ copymessage: "ruleactiontarget-folder",
+ setpriorityto: "ruleactiontarget-priority",
+ setjunkscore: "ruleactiontarget-junkscore",
+ forwardmessage: "ruleactiontarget-forwardto",
+ replytomessage: "ruleactiontarget-replyto",
+ addtagtomessage: "ruleactiontarget-tag",
+ };
+ const elementName = elementMapping[type];
+
+ return elementName ? document.createXULElement(elementName) : null;
+ }
+
+ _updateAttributes() {
+ if (!this.hasAttribute("type")) {
+ return;
+ }
+
+ const type = this.getAttribute("type");
+
+ while (this.lastChild) {
+ this.lastChild.remove();
+ }
+
+ if (type == null) {
+ return;
+ }
+
+ this.node = this._getChildNode(type);
+
+ if (this.node) {
+ this.node.setAttribute("flex", "1");
+ this.appendChild(this.node);
+ } else {
+ updateParentNode(this.closest(".ruleaction"));
+ }
+ }
+ }
+
+ customElements.define("ruleactiontarget-tag", MozRuleactiontargetTag);
+ customElements.define(
+ "ruleactiontarget-priority",
+ MozRuleactiontargetPriority
+ );
+ customElements.define(
+ "ruleactiontarget-junkscore",
+ MozRuleactiontargetJunkscore
+ );
+ customElements.define("ruleactiontarget-replyto", MozRuleactiontargetReplyto);
+ customElements.define(
+ "ruleactiontarget-forwardto",
+ MozRuleactiontargetForwardto
+ );
+ customElements.define("ruleactiontarget-folder", MozRuleactiontargetFolder);
+ customElements.define("ruleactiontarget-wrapper", MozRuleactiontargetWrapper);
+
+ /**
+ * This is an abstract class for search menulist general functionality.
+ *
+ * @abstract
+ * @augments MozXULElement
+ */
+ class MozSearchMenulistAbstract extends MozXULElement {
+ static get observedAttributes() {
+ return ["flex", "disabled"];
+ }
+
+ constructor() {
+ super();
+ this.internalScope = null;
+ this.internalValue = -1;
+ this.validityManager = Cc[
+ "@mozilla.org/mail/search/validityManager;1"
+ ].getService(Ci.nsIMsgSearchValidityManager);
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+
+ this.menulist = document.createXULElement("menulist");
+ this.menulist.classList.add("search-menulist");
+ this.menulist.addEventListener("command", this.onSelect.bind(this));
+ this.menupopup = document.createXULElement("menupopup");
+ this.menupopup.classList.add("search-menulist-popup");
+ this.menulist.appendChild(this.menupopup);
+ this.appendChild(this.menulist);
+ this._updateAttributes();
+ }
+
+ attributeChangedCallback() {
+ this._updateAttributes();
+ }
+
+ _updateAttributes() {
+ if (!this.menulist) {
+ return;
+ }
+ if (this.hasAttribute("flex")) {
+ this.menulist.setAttribute("flex", this.getAttribute("flex"));
+ } else {
+ this.menulist.removeAttribute("flex");
+ }
+ if (this.hasAttribute("disabled")) {
+ this.menulist.setAttribute("disabled", this.getAttribute("disabled"));
+ } else {
+ this.menulist.removeAttribute("disabled");
+ }
+ }
+
+ set searchScope(val) {
+ // if scope isn't changing this is a noop
+ if (this.internalScope == val) {
+ return;
+ }
+ this.internalScope = val;
+ this.refreshList();
+ if (this.targets) {
+ this.targets.forEach(target => {
+ customElements.upgrade(target);
+ target.searchScope = val;
+ });
+ }
+ }
+
+ get searchScope() {
+ return this.internalScope;
+ }
+
+ get validityTable() {
+ return this.validityManager.getTable(this.searchScope);
+ }
+
+ get targets() {
+ const forAttrs = this.getAttribute("for");
+ if (!forAttrs) {
+ return null;
+ }
+ const targetIds = forAttrs.split(",");
+ if (targetIds.length == 0) {
+ return null;
+ }
+
+ return targetIds
+ .map(id => document.getElementById(id))
+ .filter(e => e != null);
+ }
+
+ get optargets() {
+ const forAttrs = this.getAttribute("opfor");
+ if (!forAttrs) {
+ return null;
+ }
+ const optargetIds = forAttrs.split(",");
+ if (optargetIds.length == 0) {
+ return null;
+ }
+
+ return optargetIds
+ .map(id => document.getElementById(id))
+ .filter(e => e != null);
+ }
+
+ set value(val) {
+ if (this.internalValue == val) {
+ return;
+ }
+ this.internalValue = val;
+ this.menulist.selectedItem = this.validMenuitem;
+ // now notify targets of new parent's value
+ if (this.targets) {
+ this.targets.forEach(target => {
+ customElements.upgrade(target);
+ target.parentValue = val;
+ });
+ }
+ // now notify optargets of new op parent's value
+ if (this.optargets) {
+ this.optargets.forEach(optarget => {
+ customElements.upgrade(optarget);
+ optarget.opParentValue = val;
+ });
+ }
+ }
+
+ get value() {
+ return this.internalValue;
+ }
+
+ /**
+ * Gets the label of the menulist's selected item.
+ */
+ get label() {
+ return this.menulist.selectedItem.getAttribute("label");
+ }
+
+ get validMenuitem() {
+ if (this.value == -1) {
+ // -1 means not initialized
+ return null;
+ }
+ let isCustom = isNaN(this.value);
+ let typedValue = isCustom ? this.value : parseInt(this.value);
+ // custom attribute to style the unavailable menulist item
+ this.menulist.setAttribute(
+ "unavailable",
+ !this.valueIds.includes(typedValue) ? "true" : null
+ );
+ // add a hidden menulist item if value is missing
+ let menuitem = this.menulist.querySelector(`[value="${this.value}"]`);
+ if (!menuitem) {
+ // need to add a hidden menuitem
+ menuitem = this.menulist.appendItem(this.valueLabel, this.value);
+ menuitem.hidden = true;
+ }
+ return menuitem;
+ }
+
+ refreshList(dontRestore) {
+ const menuItemIds = this.valueIds;
+ const menuItemStrings = this.valueStrings;
+ const popup = this.menupopup;
+ // save our old "value" so we can restore it later
+ let oldData;
+ if (!dontRestore) {
+ oldData = this.menulist.value;
+ }
+ // remove the old popup children
+ while (popup.hasChildNodes()) {
+ popup.lastChild.remove();
+ }
+ let newSelection;
+ let customizePos = -1;
+ for (let i = 0; i < menuItemIds.length; i++) {
+ // create the menuitem
+ if (Ci.nsMsgSearchAttrib.OtherHeader == menuItemIds[i].toString()) {
+ customizePos = i;
+ } else {
+ const menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("label", menuItemStrings[i]);
+ menuitem.setAttribute("value", menuItemIds[i]);
+ popup.appendChild(menuitem);
+ // try to restore the selection
+ if (!newSelection || oldData == menuItemIds[i].toString()) {
+ newSelection = menuitem;
+ }
+ }
+ }
+ if (customizePos != -1) {
+ const separator = document.createXULElement("menuseparator");
+ popup.appendChild(separator);
+ const menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("label", menuItemStrings[customizePos]);
+ menuitem.setAttribute("value", menuItemIds[customizePos]);
+ popup.appendChild(menuitem);
+ }
+
+ // If we are either uninitialized, or if we are called because
+ // of a change in our parent, update the value to the
+ // default stored in newSelection.
+ if ((this.value == -1 || dontRestore) && newSelection) {
+ this.value = newSelection.getAttribute("value");
+ }
+ this.menulist.selectedItem = this.validMenuitem;
+ }
+
+ onSelect(event) {
+ if (this.menulist.value == Ci.nsMsgSearchAttrib.OtherHeader) {
+ // Customize menuitem selected.
+ let args = {};
+ window.openDialog(
+ "chrome://messenger/content/CustomHeaders.xhtml",
+ "",
+ "modal,centerscreen,resizable,titlebar,chrome",
+ args
+ );
+ // User may have removed the custom header currently selected
+ // in the menulist so temporarily set the selection to a safe value.
+ this.value = Ci.nsMsgSearchAttrib.OtherHeader;
+ // rebuild the menulist
+ UpdateAfterCustomHeaderChange();
+ // Find the created or chosen custom header and select it.
+ let menuitem = null;
+ if (args.selectedVal) {
+ menuitem = this.menulist.querySelector(
+ `[label="${args.selectedVal}"]`
+ );
+ }
+ if (menuitem) {
+ this.value = menuitem.value;
+ } else {
+ // Nothing was picked in the custom headers editor so just pick something
+ // instead of the current "Customize" menuitem.
+ this.value = this.menulist.getItemAtIndex(0).value;
+ }
+ } else {
+ this.value = this.menulist.value;
+ }
+ }
+ }
+
+ /**
+ * The MozSearchAttribute widget is typically used in the search and filter dialogs to show a list
+ * of possible message headers.
+ *
+ * @augments MozSearchMenulistAbstract
+ */
+ class MozSearchAttribute extends MozSearchMenulistAbstract {
+ constructor() {
+ super();
+
+ this.stringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/search-attributes.properties"
+ );
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+
+ initializeTermFromId(this.id);
+ }
+
+ get valueLabel() {
+ if (isNaN(this.value)) {
+ // is this a custom term?
+ let customTerm = MailServices.filters.getCustomTerm(this.value);
+ if (customTerm) {
+ return customTerm.name;
+ }
+ // The custom term may be missing after the extension that added it
+ // was disabled or removed. We need to notify the user.
+ let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
+ Ci.nsIScriptError
+ );
+ scriptError.init(
+ "Missing custom search term " + this.value,
+ null,
+ null,
+ 0,
+ 0,
+ Ci.nsIScriptError.errorFlag,
+ "component javascript"
+ );
+ Services.console.logMessage(scriptError);
+ return this.stringBundle.GetStringFromName("MissingCustomTerm");
+ }
+ return this.stringBundle.GetStringFromName(
+ this.validityManager.getAttributeProperty(parseInt(this.value))
+ );
+ }
+
+ get valueIds() {
+ let result = this.validityTable.getAvailableAttributes();
+ // add any available custom search terms
+ for (let customTerm of MailServices.filters.getCustomTerms()) {
+ // For custom terms, the array element is a string with the custom id
+ // instead of the integer attribute
+ if (customTerm.getAvailable(this.searchScope, null)) {
+ result.push(customTerm.id);
+ }
+ }
+ return result;
+ }
+
+ get valueStrings() {
+ let strings = [];
+ let ids = this.valueIds;
+ let hdrsArray = null;
+ try {
+ let hdrs = Services.prefs.getCharPref("mailnews.customHeaders");
+ hdrs = hdrs.replace(/\s+/g, ""); // remove white spaces before splitting
+ hdrsArray = hdrs.match(/[^:]+/g);
+ } catch (ex) {}
+ let j = 0;
+ for (let i = 0; i < ids.length; i++) {
+ if (isNaN(ids[i])) {
+ // Is this a custom search term?
+ let customTerm = MailServices.filters.getCustomTerm(ids[i]);
+ if (customTerm) {
+ strings[i] = customTerm.name;
+ } else {
+ strings[i] = "";
+ }
+ } else if (ids[i] > Ci.nsMsgSearchAttrib.OtherHeader && hdrsArray) {
+ strings[i] = hdrsArray[j++];
+ } else {
+ strings[i] = this.stringBundle.GetStringFromName(
+ this.validityManager.getAttributeProperty(ids[i])
+ );
+ }
+ }
+ return strings;
+ }
+ }
+ customElements.define("search-attribute", MozSearchAttribute);
+
+ /**
+ * MozSearchOperator contains a list of operators that can be applied on search-attribute and
+ * search-value value.
+ *
+ * @augments MozSearchMenulistAbstract
+ */
+ class MozSearchOperator extends MozSearchMenulistAbstract {
+ constructor() {
+ super();
+
+ this.stringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/search-operators.properties"
+ );
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+
+ this.searchAttribute = Ci.nsMsgSearchAttrib.Default;
+ }
+
+ get valueLabel() {
+ return this.stringBundle.GetStringFromName(this.value);
+ }
+
+ get valueIds() {
+ let isCustom = isNaN(this.searchAttribute);
+ if (isCustom) {
+ let customTerm = MailServices.filters.getCustomTerm(
+ this.searchAttribute
+ );
+ if (customTerm) {
+ return customTerm.getAvailableOperators(this.searchScope);
+ }
+ return [Ci.nsMsgSearchOp.Contains];
+ }
+ return this.validityTable.getAvailableOperators(this.searchAttribute);
+ }
+
+ get valueStrings() {
+ let strings = [];
+ let ids = this.valueIds;
+ for (let i = 0; i < ids.length; i++) {
+ strings[i] = this.stringBundle.GetStringFromID(ids[i]);
+ }
+ return strings;
+ }
+
+ set parentValue(val) {
+ if (
+ this.searchAttribute == val &&
+ val != Ci.nsMsgSearchAttrib.OtherHeader
+ ) {
+ return;
+ }
+ this.searchAttribute = val;
+ this.refreshList(true); // don't restore the selection, since searchvalue nulls it
+ if (val == Ci.nsMsgSearchAttrib.AgeInDays) {
+ // We want "Age in Days" to default to "is less than".
+ this.value = Ci.nsMsgSearchOp.IsLessThan;
+ }
+ }
+
+ get parentValue() {
+ return this.searchAttribute;
+ }
+ }
+ customElements.define("search-operator", MozSearchOperator);
+
+ /**
+ * MozSearchValue is a widget that allows selecting the value to search or filter on. It can be a
+ * text entry, priority, status, junk status, tags, hasAttachment status, and addressbook etc.
+ *
+ * @augments MozXULElement
+ */
+ class MozSearchValue extends MozXULElement {
+ static get observedAttributes() {
+ return ["disabled"];
+ }
+
+ constructor() {
+ super();
+
+ this.addEventListener("keypress", event => {
+ if (event.keyCode != KeyEvent.DOM_VK_RETURN) {
+ return;
+ }
+ onEnterInSearchTerm(event);
+ });
+
+ this.internalOperator = null;
+ this.internalAttribute = null;
+ this.internalValue = null;
+
+ this.inputType = "none";
+ }
+
+ connectedCallback() {
+ this.classList.add("input-container");
+ }
+
+ static get stringBundle() {
+ if (!this._stringBundle) {
+ this._stringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ }
+ return this._stringBundle;
+ }
+
+ /**
+ * Create a menulist to be used as the input.
+ *
+ * @param {object[]} itemDataList - An ordered list of items to add to the
+ * menulist. Each entry must have a 'value' property to be used as the
+ * item value. If the entry has a 'label' property, it will be used
+ * directly as the item label, otherwise it must identify a bundle string
+ * using the 'stringId' property.
+ *
+ * @returns {MozMenuList} - The newly created menulist.
+ */
+ static _createMenulist(itemDataList) {
+ let menulist = document.createXULElement("menulist");
+ menulist.classList.add("search-value-menulist");
+ let menupopup = document.createXULElement("menupopup");
+ menupopup.classList.add("search-value-popup");
+
+ let bundle = this.stringBundle;
+
+ for (let itemData of itemDataList) {
+ let item = document.createXULElement("menuitem");
+ item.classList.add("search-value-menuitem");
+ item.label =
+ itemData.label || bundle.GetStringFromName(itemData.stringId);
+ item.value = itemData.value;
+ menupopup.appendChild(item);
+ }
+ menulist.appendChild(menupopup);
+ return menulist;
+ }
+
+ /**
+ * Set the child input. The input will only be changed if the type changes.
+ *
+ * @param {string} type - The type of input to use.
+ * @param {string|number|undefined} value - A value to set on the input, or
+ * leave undefined to not change the value. See setInputValue.
+ */
+ setInput(type, value) {
+ if (type != this.inputType) {
+ this.inputType = type;
+ this.input?.remove();
+ let input;
+ switch (type) {
+ case "text":
+ input = document.createElement("input");
+ input.classList.add("input-inline", "search-value-input");
+ break;
+ case "date":
+ input = document.createElement("input");
+ input.classList.add("input-inline", "search-value-input");
+ if (!value) {
+ // Newly created date input shows today's date.
+ // value is expected in microseconds since epoch.
+ value = Date.now() * 1000;
+ }
+ break;
+ case "size":
+ input = document.createElement("input");
+ input.type = "number";
+ input.min = 0;
+ input.max = 1000000000;
+ input.classList.add("input-inline", "search-value-input");
+ break;
+ case "age":
+ input = document.createElement("input");
+ input.type = "number";
+ input.min = -40000; // ~100 years.
+ input.max = 40000;
+ input.classList.add("input-inline", "search-value-input");
+ break;
+ case "percent":
+ input = document.createElement("input");
+ input.type = "number";
+ input.min = 0;
+ input.max = 100;
+ input.classList.add("input-inline", "search-value-input");
+ break;
+ case "priority":
+ input = this.constructor._createMenulist([
+ { stringId: "priorityHighest", value: Ci.nsMsgPriority.highest },
+ { stringId: "priorityHigh", value: Ci.nsMsgPriority.high },
+ { stringId: "priorityNormal", value: Ci.nsMsgPriority.normal },
+ { stringId: "priorityLow", value: Ci.nsMsgPriority.low },
+ { stringId: "priorityLowest", value: Ci.nsMsgPriority.lowest },
+ ]);
+ break;
+ case "status":
+ input = this.constructor._createMenulist([
+ { stringId: "replied", value: Ci.nsMsgMessageFlags.Replied },
+ { stringId: "read", value: Ci.nsMsgMessageFlags.Read },
+ { stringId: "new", value: Ci.nsMsgMessageFlags.New },
+ { stringId: "forwarded", value: Ci.nsMsgMessageFlags.Forwarded },
+ { stringId: "flagged", value: Ci.nsMsgMessageFlags.Marked },
+ ]);
+ break;
+ case "addressbook":
+ input = document.createXULElement("menulist", {
+ is: "menulist-addrbooks",
+ });
+ input.setAttribute("localonly", "true");
+ input.classList.add("search-value-menulist");
+ if (!value) {
+ // Select the personal addressbook by default.
+ value = "jsaddrbook://abook.sqlite";
+ }
+ break;
+ case "tags":
+ input = this.constructor._createMenulist(
+ MailServices.tags.getAllTags().map(taginfo => {
+ return { label: taginfo.tag, value: taginfo.key };
+ })
+ );
+ break;
+ case "junk-status":
+ // "Junk Status is/isn't/is empty/isn't empty 'Junk'".
+ input = this.constructor._createMenulist([
+ { stringId: "junk", value: Ci.nsIJunkMailPlugin.JUNK },
+ ]);
+ break;
+ case "attachment-status":
+ // "Attachment Status is/isn't 'Has Attachments'".
+ input = this.constructor._createMenulist([
+ { stringId: "hasAttachments", value: "0" },
+ ]);
+ break;
+ case "junk-origin":
+ input = this.constructor._createMenulist([
+ { stringId: "junkScoreOriginPlugin", value: "plugin" },
+ { stringId: "junkScoreOriginUser", value: "user" },
+ { stringId: "junkScoreOriginFilter", value: "filter" },
+ { stringId: "junkScoreOriginWhitelist", value: "whitelist" },
+ { stringId: "junkScoreOriginImapFlag", value: "imapflag" },
+ ]);
+ break;
+ case "none":
+ input = null;
+ break;
+ case "custom":
+ // Used by extensions.
+ // FIXME: We need a better way for extensions to set a custom input.
+ input = document.createXULElement("hbox");
+ input.setAttribute("flex", "1");
+ input.classList.add("search-value-custom");
+ break;
+ default:
+ throw new Error(`Unrecognised input type "${type}"`);
+ }
+
+ this.input = input;
+ if (input) {
+ this.appendChild(input);
+ }
+
+ this._updateAttributes();
+ }
+
+ this.setInputValue(value);
+ }
+
+ /**
+ * Set the child input to the given value.
+ *
+ * @param {string|number} value - The value to set on the input. For "date"
+ * inputs, this should be a number of microseconds since the epoch.
+ */
+ setInputValue(value) {
+ if (value === undefined) {
+ return;
+ }
+ switch (this.inputType) {
+ case "text":
+ case "size":
+ case "age":
+ case "percent":
+ this.input.value = value;
+ break;
+ case "date":
+ this.input.value = convertPRTimeToString(value);
+ break;
+ case "priority":
+ case "status":
+ case "addressbook":
+ case "tags":
+ case "junk-status":
+ case "attachment-status":
+ case "junk-origin":
+ let item = this.input.querySelector(`menuitem[value="${value}"]`);
+ if (item) {
+ this.input.selectedItem = item;
+ }
+ break;
+ case "none":
+ // Silently ignore the value.
+ break;
+ case "custom":
+ this.input.setAttribute("value", value);
+ break;
+ default:
+ throw new Error(`Unhandled input type "${this.inputType}"`);
+ }
+ }
+
+ /**
+ * Get the child input's value.
+ *
+ * @returns {string|number} - The value set in the input. For "date"
+ * inputs, this is the number of microseconds since the epoch.
+ */
+ getInputValue() {
+ switch (this.inputType) {
+ case "text":
+ case "size":
+ case "age":
+ case "percent":
+ return this.input.value;
+ case "date":
+ return convertStringToPRTime(this.input.value);
+ case "priority":
+ case "status":
+ case "addressbook":
+ case "tags":
+ case "junk-status":
+ case "attachment-status":
+ case "junk-origin":
+ return this.input.selectedItem.value;
+ case "none":
+ return "";
+ case "custom":
+ return this.input.getAttribute("value");
+ default:
+ throw new Error(`Unhandled input type "${this.inputType}"`);
+ }
+ }
+
+ /**
+ * Get the element's displayed value.
+ *
+ * @returns {string} - The value seen by the user.
+ */
+ getReadableValue() {
+ switch (this.inputType) {
+ case "text":
+ case "size":
+ case "age":
+ case "percent":
+ case "date":
+ return this.input.value;
+ case "priority":
+ case "status":
+ case "addressbook":
+ case "tags":
+ case "junk-status":
+ case "attachment-status":
+ case "junk-origin":
+ return this.input.selectedItem.label;
+ case "none":
+ return "";
+ case "custom":
+ return this.input.getAttribute("value");
+ default:
+ throw new Error(`Unhandled input type "${this.inputType}"`);
+ }
+ }
+
+ attributeChangedCallback() {
+ this._updateAttributes();
+ }
+
+ _updateAttributes() {
+ if (!this.input) {
+ return;
+ }
+ if (this.hasAttribute("disabled")) {
+ this.input.setAttribute("disabled", this.getAttribute("disabled"));
+ } else {
+ this.input.removeAttribute("disabled");
+ }
+ }
+
+ /**
+ * Update the displayed input according to the selected sibling attributes
+ * and operators.
+ *
+ * @param {nsIMsgSearchValue} [value] - A value to display in the input. Or
+ * leave unset to not change the value.
+ */
+ updateDisplay(value) {
+ let operator = Number(this.internalOperator);
+ switch (Number(this.internalAttribute)) {
+ // Use the index to hide/show the appropriate child.
+ case Ci.nsMsgSearchAttrib.Priority:
+ this.setInput("priority", value?.priority);
+ break;
+ case Ci.nsMsgSearchAttrib.MsgStatus:
+ this.setInput("status", value?.status);
+ break;
+ case Ci.nsMsgSearchAttrib.Date:
+ this.setInput("date", value?.date);
+ break;
+ case Ci.nsMsgSearchAttrib.Sender:
+ case Ci.nsMsgSearchAttrib.To:
+ case Ci.nsMsgSearchAttrib.ToOrCC:
+ case Ci.nsMsgSearchAttrib.AllAddresses:
+ case Ci.nsMsgSearchAttrib.CC:
+ if (
+ operator == Ci.nsMsgSearchOp.IsntInAB ||
+ operator == Ci.nsMsgSearchOp.IsInAB
+ ) {
+ this.setInput("addressbook", value?.str);
+ } else {
+ this.setInput("text", value?.str);
+ }
+ break;
+ case Ci.nsMsgSearchAttrib.Keywords:
+ this.setInput(
+ operator == Ci.nsMsgSearchOp.IsEmpty ||
+ operator == Ci.nsMsgSearchOp.IsntEmpty
+ ? "none"
+ : "tags",
+ value?.str
+ );
+ break;
+ case Ci.nsMsgSearchAttrib.JunkStatus:
+ this.setInput(
+ operator == Ci.nsMsgSearchOp.IsEmpty ||
+ operator == Ci.nsMsgSearchOp.IsntEmpty
+ ? "none"
+ : "junk-status",
+ value?.junkStatus
+ );
+ break;
+ case Ci.nsMsgSearchAttrib.HasAttachmentStatus:
+ this.setInput("attachment-status", value?.hasAttachmentStatus);
+ break;
+ case Ci.nsMsgSearchAttrib.JunkScoreOrigin:
+ this.setInput("junk-origin", value?.str);
+ break;
+ case Ci.nsMsgSearchAttrib.AgeInDays:
+ this.setInput("age", value?.age);
+ break;
+ case Ci.nsMsgSearchAttrib.Size:
+ this.setInput("size", value?.size);
+ break;
+ case Ci.nsMsgSearchAttrib.JunkPercent:
+ this.setInput("percent", value?.junkPercent);
+ break;
+ default:
+ if (isNaN(this.internalAttribute)) {
+ // Custom attribute, the internalAttribute is a string.
+ // FIXME: We need a better way for extensions to set a custom input.
+ this.setInput("custom", value?.str);
+ this.input.setAttribute("searchAttribute", this.internalAttribute);
+ } else {
+ this.setInput("text", value?.str);
+ }
+ break;
+ }
+ }
+
+ /**
+ * The sibling operator type.
+ *
+ * @type {nsMsgSearchOpValue}
+ */
+ set opParentValue(val) {
+ if (this.internalOperator == val) {
+ return;
+ }
+ this.internalOperator = val;
+ this.updateDisplay();
+ }
+
+ get opParentValue() {
+ return this.internalOperator;
+ }
+
+ /**
+ * A duplicate of the searchAttribute property.
+ *
+ * @type {nsMsgSearchAttribValue}
+ */
+ set parentValue(val) {
+ this.searchAttribute = val;
+ }
+
+ get parentValue() {
+ return this.searchAttribute;
+ }
+
+ /**
+ * The sibling attribute type.
+ *
+ * @type {nsMsgSearchAttribValue}
+ */
+ set searchAttribute(val) {
+ if (this.internalAttribute == val) {
+ return;
+ }
+ this.internalAttribute = val;
+ this.updateDisplay();
+ }
+
+ get searchAttribute() {
+ return this.internalAttribute;
+ }
+
+ /**
+ * The stored value for this element.
+ *
+ * Note that the input value is *derived* from this object when it is set.
+ * But changes to the input value using the UI will not change the stored
+ * value until the save method is called.
+ *
+ * @type {nsIMsgSearchValue}
+ */
+ set value(val) {
+ // val is a nsIMsgSearchValue object
+ this.internalValue = val;
+ this.updateDisplay(val);
+ }
+
+ get value() {
+ return this.internalValue;
+ }
+
+ /**
+ * Updates the stored value for this element to reflect its current input
+ * value.
+ */
+ save() {
+ let searchValue = this.value;
+ let searchAttribute = this.searchAttribute;
+
+ searchValue.attrib = isNaN(searchAttribute)
+ ? Ci.nsMsgSearchAttrib.Custom
+ : searchAttribute;
+ switch (Number(searchAttribute)) {
+ case Ci.nsMsgSearchAttrib.Priority:
+ searchValue.priority = this.getInputValue();
+ break;
+ case Ci.nsMsgSearchAttrib.MsgStatus:
+ searchValue.status = this.getInputValue();
+ break;
+ case Ci.nsMsgSearchAttrib.AgeInDays:
+ searchValue.age = this.getInputValue();
+ break;
+ case Ci.nsMsgSearchAttrib.Date:
+ searchValue.date = this.getInputValue();
+ break;
+ case Ci.nsMsgSearchAttrib.JunkStatus:
+ searchValue.junkStatus = this.getInputValue();
+ break;
+ case Ci.nsMsgSearchAttrib.HasAttachmentStatus:
+ searchValue.status = Ci.nsMsgMessageFlags.Attachment;
+ break;
+ case Ci.nsMsgSearchAttrib.JunkPercent:
+ searchValue.junkPercent = this.getInputValue();
+ break;
+ case Ci.nsMsgSearchAttrib.Size:
+ searchValue.size = this.getInputValue();
+ break;
+ default:
+ searchValue.str = this.getInputValue();
+ break;
+ }
+ }
+
+ /**
+ * Stores the displayed value for this element in the given object.
+ *
+ * Note that after this call, the stored value will remain pointing to the
+ * given searchValue object.
+ *
+ * @param {nsIMsgSearchValue} searchValue - The object to store the
+ * displayed value in.
+ */
+ saveTo(searchValue) {
+ this.internalValue = searchValue;
+ this.save();
+ }
+ }
+ customElements.define("search-value", MozSearchValue);
+
+ // The menulist CE is defined lazily. Create one now to get menulist defined,
+ // allowing us to inherit from it.
+ if (!customElements.get("menulist")) {
+ delete document.createXULElement("menulist");
+ }
+ {
+ /**
+ * The MozRuleactiontypeMenulist is a widget that allows selecting the actions from the given menulist for
+ * the selected folder. It gets displayed in the message filter dialog box.
+ *
+ * @augments {MozMenuList}
+ */
+ class MozRuleactiontypeMenulist extends customElements.get("menulist") {
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.delayConnectedCallback() || this.hasConnected) {
+ return;
+ }
+ this.hasConnected = true;
+
+ this.setAttribute("is", "ruleactiontype-menulist");
+ this.addEventListener("command", event => {
+ this.parentNode.setAttribute("value", this.value);
+ checkActionsReorder();
+ });
+
+ this.addEventListener("popupshowing", event => {
+ let unavailableActions = this.usedActionsList();
+ for (let index = 0; index < this.menuitems.length; index++) {
+ let menu = this.menuitems[index];
+ menu.setAttribute("disabled", menu.value in unavailableActions);
+ }
+ });
+
+ this.menuitems = this.getElementsByTagNameNS(
+ this.namespaceURI,
+ "menuitem"
+ );
+
+ // Force initialization of the menulist custom element first.
+ customElements.upgrade(this);
+ 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")) {
+ let unavailableActions = this.usedActionsList();
+ // Select the first one that's not in the list.
+ for (let index = 0; index < this.menuitems.length; index++) {
+ let menu = this.menuitems[index];
+ if (!(menu.value in unavailableActions) && !menu.hidden) {
+ this.value = menu.value;
+ this.parentNode.setAttribute("value", menu.value);
+ break;
+ }
+ }
+ } else {
+ this.parentNode.mActionTypeInitialized = true;
+ this.parentNode.clearInitialActionIndex();
+ }
+ }
+
+ hideInvalidActions() {
+ let menupopup = this.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;
+
+ // Disable / enable all elements in the "filteractionlist"
+ // based on the scope and the "enablefornews" attribute.
+ elements = menupopup.getElementsByAttribute("enablefornews", "true");
+ for (let i = 0; i < elements.length; i++) {
+ elements[i].hidden = scope != Ci.nsMsgSearchScope.newsFilter;
+ }
+
+ elements = menupopup.getElementsByAttribute("enablefornews", "false");
+ for (let i = 0; i < elements.length; i++) {
+ elements[i].hidden = scope == Ci.nsMsgSearchScope.newsFilter;
+ }
+
+ elements = menupopup.getElementsByAttribute("enableforpop3", "true");
+ for (let 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 (let 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.findTemplates().length == 0) {
+ elements = menupopup.getElementsByAttribute(
+ "value",
+ "replytomessage"
+ );
+ if (elements.length == 1) {
+ elements[0].hidden = true;
+ }
+ }
+ }
+
+ addCustomActions() {
+ let menupopup = this.menupopup;
+ for (let i = 0; i < gCustomActions.length; i++) {
+ let customAction = gCustomActions[i];
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute("label", customAction.name);
+ menuitem.setAttribute("value", customAction.id);
+ menuitem.setAttribute("isCustom", "true");
+ menupopup.appendChild(menuitem);
+ }
+ }
+
+ /**
+ * Returns a hash containing all of the filter actions which are currently
+ * being used by other filteractionrows.
+ *
+ * @returns {object} - a hash containing all of the filter actions which are
+ * currently being used by other filteractionrows.
+ */
+ usedActionsList() {
+ let usedActions = {};
+ let currentFilterActionRow = this.parentNode;
+ let listBox = currentFilterActionRow.parentNode; // need to account for the list item.
+ // Now iterate over each list item in the list box.
+ for (let index = 0; index < listBox.getRowCount(); index++) {
+ let filterActionRow = listBox.getItemAtIndex(index);
+ if (filterActionRow != currentFilterActionRow) {
+ let actionValue = filterActionRow.getAttribute("value");
+
+ // Let custom actions decide if dups are allowed.
+ let isCustom = false;
+ for (let 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;
+ } else if (actionValue == "markasread") {
+ // The same with Mark as read/Mark as Unread.
+ usedActions.markasunread = true;
+ } else if (actionValue == "markasunread") {
+ usedActions.markasread = true;
+ }
+ }
+ }
+ }
+ return usedActions;
+ }
+
+ /**
+ * Check if there exist any templates in this account.
+ *
+ * @returns {object[]} - An array of template headers: each has a label and
+ * a value.
+ */
+ findTemplates() {
+ let identities = MailServices.accounts.getIdentitiesForServer(
+ gFilterList.folder.server
+ );
+ // Typically if this is Local Folders.
+ if (identities.length == 0) {
+ if (MailServices.accounts.defaultAccount) {
+ identities.push(
+ MailServices.accounts.defaultAccount.defaultIdentity
+ );
+ }
+ }
+
+ let templates = [];
+ let foldersScanned = [];
+
+ for (let identity of identities) {
+ let enumerator = null;
+ let msgFolder = MailUtils.getExistingFolder(
+ identity.stationeryFolder
+ );
+ // If we already processed this folder, do not set enumerator
+ // so that we skip this identity.
+ if (msgFolder && !foldersScanned.includes(msgFolder)) {
+ foldersScanned.push(msgFolder);
+ enumerator = msgFolder.msgDatabase.enumerateMessages();
+ }
+
+ if (!enumerator) {
+ continue;
+ }
+
+ for (let header of enumerator) {
+ let uri =
+ msgFolder.URI +
+ "?messageId=" +
+ header.messageId +
+ "&subject=" +
+ header.mime2DecodedSubject;
+ templates.push({ label: header.mime2DecodedSubject, value: uri });
+ }
+ }
+ return templates;
+ }
+ }
+
+ customElements.define(
+ "ruleactiontype-menulist",
+ MozRuleactiontypeMenulist,
+ { extends: "menulist" }
+ );
+ }
+
+ /**
+ * The MozRuleactionRichlistitem is a widget which gives the options to filter
+ * the messages with following elements: ruleactiontype-menulist, ruleactiontarget-wrapper
+ * and button to add or remove the MozRuleactionRichlistitem. It gets added in the
+ * filterActionList richlistbox in the Filter Editor dialog.
+ *
+ * @augments {MozElements.MozRichlistitem}
+ */
+ class MozRuleactionRichlistitem extends MozElements.MozRichlistitem {
+ static get inheritedAttributes() {
+ return { ".ruleactiontarget": "type=value" };
+ }
+
+ constructor() {
+ super();
+
+ this.mActionTypeInitialized = false;
+ this.mRuleActionTargetInitialized = false;
+ }
+
+ connectedCallback() {
+ if (this.delayConnectedCallback() || this.hasChildNodes()) {
+ return;
+ }
+ this.setAttribute("is", "ruleaction-richlistitem");
+ this.appendChild(
+ MozXULElement.parseXULToFragment(
+ `
+ <menulist is="ruleactiontype-menulist" style="flex: &filterActionTypeFlexValue;">
+ <menupopup>
+ <menuitem label="&moveMessage.label;"
+ value="movemessage"
+ enablefornews="false"></menuitem>
+ <menuitem label="&copyMessage.label;"
+ value="copymessage"></menuitem>
+ <menuseparator enablefornews="false"></menuseparator>
+ <menuitem label="&forwardTo.label;"
+ value="forwardmessage"
+ enablefornews="false"></menuitem>
+ <menuitem label="&replyWithTemplate.label;"
+ value="replytomessage"
+ enablefornews="false"></menuitem>
+ <menuseparator></menuseparator>
+ <menuitem label="&markMessageRead.label;"
+ value="markasread"></menuitem>
+ <menuitem label="&markMessageUnread.label;"
+ value="markasunread"></menuitem>
+ <menuitem label="&markMessageStarred.label;"
+ value="markasflagged"></menuitem>
+ <menuitem label="&setPriority.label;"
+ value="setpriorityto"></menuitem>
+ <menuitem label="&addTag.label;"
+ value="addtagtomessage"></menuitem>
+ <menuitem label="&setJunkScore.label;"
+ value="setjunkscore"
+ enablefornews="false"></menuitem>
+ <menuseparator enableforpop3="true"></menuseparator>
+ <menuitem label="&deleteMessage.label;"
+ value="deletemessage"></menuitem>
+ <menuitem label="&deleteFromPOP.label;"
+ value="deletefrompopserver"
+ enableforpop3="true"></menuitem>
+ <menuitem label="&fetchFromPOP.label;"
+ value="fetchfrompopserver"
+ enableforpop3="true"></menuitem>
+ <menuseparator></menuseparator>
+ <menuitem label="&ignoreThread.label;"
+ value="ignorethread"></menuitem>
+ <menuitem label="&ignoreSubthread.label;"
+ value="ignoresubthread"></menuitem>
+ <menuitem label="&watchThread.label;"
+ value="watchthread"></menuitem>
+ <menuseparator></menuseparator>
+ <menuitem label="&stopExecution.label;"
+ value="stopexecution"></menuitem>
+ </menupopup>
+ </menulist>
+ <ruleactiontarget-wrapper class="ruleactiontarget"
+ style="flex: &filterActionTargetFlexValue;">
+ </ruleactiontarget-wrapper>
+ <hbox>
+ <button class="small-button"
+ label="+"
+ tooltiptext="&addAction.tooltip;"
+ oncommand="this.parentNode.parentNode.addRow();"></button>
+ <button class="small-button remove-small-button"
+ label="−"
+ tooltiptext="&removeAction.tooltip;"
+ oncommand="this.parentNode.parentNode.removeRow();"></button>
+ </hbox>
+ `,
+ [
+ "chrome://messenger/locale/messenger.dtd",
+ "chrome://messenger/locale/FilterEditor.dtd",
+ ]
+ )
+ );
+
+ this.mRuleActionType = this.querySelector("menulist");
+ this.mRemoveButton = this.querySelector(".remove-small-button");
+ this.mListBox = this.parentNode;
+ this.initializeAttributeInheritance();
+ }
+
+ set selected(val) {
+ // This provides a dummy selected property that the richlistbox expects to
+ // be able to call. See bug 202036.
+ }
+
+ get selected() {
+ return false;
+ }
+
+ _fireEvent(aName) {
+ // This provides a dummy _fireEvent function that the richlistbox expects to
+ // be able to call. See bug 202036.
+ }
+
+ /**
+ * 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.
+ */
+ clearInitialActionIndex() {
+ if (this.mActionTypeInitialized && this.mRuleActionTargetInitialized) {
+ this.removeAttribute("initialActionIndex");
+ }
+ }
+
+ initWithAction(aFilterAction) {
+ let filterActionStr;
+ let actionTarget = this.children[1];
+ let actionItem = actionTarget.ruleactiontargetElement;
+ let nsMsgFilterAction = Ci.nsMsgFilterAction;
+ switch (aFilterAction.type) {
+ case nsMsgFilterAction.Custom:
+ filterActionStr = aFilterAction.customId;
+ if (actionItem) {
+ actionItem.children[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.
+ let needCustomLabel = true;
+ for (let i = 0; i < gCustomActions.length; i++) {
+ if (gCustomActions[i].id == filterActionStr) {
+ needCustomLabel = false;
+ break;
+ }
+ }
+ if (needCustomLabel) {
+ let menuitem = document.createXULElement("menuitem");
+ menuitem.setAttribute(
+ "label",
+ gFilterBundle.getString("filterMissingCustomAction")
+ );
+ menuitem.setAttribute("value", filterActionStr);
+ menuitem.disabled = true;
+ this.mRuleActionType.menupopup.appendChild(menuitem);
+ let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
+ Ci.nsIScriptError
+ );
+ scriptError.init(
+ "Missing custom action " + filterActionStr,
+ null,
+ null,
+ 0,
+ 0,
+ Ci.nsIScriptError.errorFlag,
+ "component javascript"
+ );
+ Services.console.logMessage(scriptError);
+ }
+ break;
+ case nsMsgFilterAction.MoveToFolder:
+ case nsMsgFilterAction.CopyToFolder:
+ actionItem.children[0].value = aFilterAction.targetFolderUri;
+ break;
+ case nsMsgFilterAction.Reply:
+ case nsMsgFilterAction.Forward:
+ actionItem.children[0].value = aFilterAction.strValue;
+ break;
+ case nsMsgFilterAction.ChangePriority:
+ actionItem.children[0].value = aFilterAction.priority;
+ break;
+ case nsMsgFilterAction.JunkScore:
+ actionItem.children[0].value = aFilterAction.junkScore;
+ break;
+ case nsMsgFilterAction.AddTag:
+ actionItem.children[0].value = aFilterAction.strValue;
+ break;
+ default:
+ break;
+ }
+ if (aFilterAction.type != nsMsgFilterAction.Custom) {
+ filterActionStr = gFilterActionStrings[aFilterAction.type];
+ }
+ this.mRuleActionType.value = filterActionStr;
+ this.mRuleActionTargetInitialized = true;
+ this.clearInitialActionIndex();
+ checkActionsReorder();
+ }
+
+ /**
+ * Function is used to check if the filter is valid or not. This routine
+ * also prompts the user.
+ *
+ * @returns {boolean} - true if this row represents a valid filter action.
+ */
+ validateAction() {
+ let filterActionString = this.getAttribute("value");
+ let actionTarget = this.children[1];
+ let actionTargetLabel =
+ actionTarget.ruleactiontargetElement &&
+ actionTarget.ruleactiontargetElement.children[0].value;
+ let errorString, customError;
+
+ switch (filterActionString) {
+ case "movemessage":
+ case "copymessage":
+ let msgFolder = actionTargetLabel
+ ? MailUtils.getOrCreateFolder(actionTargetLabel)
+ : null;
+ if (!msgFolder || !msgFolder.canFileMessages) {
+ errorString = "mustSelectFolder";
+ }
+ break;
+ case "forwardmessage":
+ if (
+ actionTargetLabel.length < 3 ||
+ actionTargetLabel.indexOf("@") < 1
+ ) {
+ errorString = "enterValidEmailAddress";
+ }
+ break;
+ case "replytomessage":
+ if (!actionTarget.ruleactiontargetElement.children[0].selectedItem) {
+ errorString = "pickTemplateToReplyWith";
+ }
+ break;
+ default:
+ // Locate the correct custom action, and check validity.
+ for (let i = 0; i < gCustomActions.length; i++) {
+ if (gCustomActions[i].id == filterActionString) {
+ customError = gCustomActions[i].validateActionValue(
+ actionTargetLabel,
+ gFilterList.folder,
+ gFilterType
+ );
+ break;
+ }
+ }
+ break;
+ }
+
+ errorString = errorString
+ ? gFilterBundle.getString(errorString)
+ : customError;
+ if (errorString) {
+ Services.prompt.alert(window, null, errorString);
+ }
+
+ return !errorString;
+ }
+
+ /**
+ * Create a new filter action, fill it in, and then append it to the filter.
+ *
+ * @param {object} aFilter - filter object to save.
+ */
+ saveToFilter(aFilter) {
+ let filterAction = aFilter.createAction();
+ let filterActionString = this.getAttribute("value");
+ filterAction.type = gFilterActionStrings.indexOf(filterActionString);
+ let actionTarget = this.children[1];
+ let actionItem = actionTarget.ruleactiontargetElement;
+ let nsMsgFilterAction = Ci.nsMsgFilterAction;
+ switch (filterAction.type) {
+ case nsMsgFilterAction.ChangePriority:
+ filterAction.priority = actionItem.children[0].getAttribute("value");
+ break;
+ case nsMsgFilterAction.MoveToFolder:
+ case nsMsgFilterAction.CopyToFolder:
+ filterAction.targetFolderUri = actionItem.children[0].value;
+ break;
+ case nsMsgFilterAction.JunkScore:
+ filterAction.junkScore = actionItem.children[0].value;
+ break;
+ case nsMsgFilterAction.Custom:
+ filterAction.customId = filterActionString;
+ // Fall through to set the value.
+ default:
+ if (actionItem && actionItem.children.length > 0) {
+ filterAction.strValue = actionItem.children[0].value;
+ }
+ break;
+ }
+ aFilter.appendAction(filterAction);
+ }
+
+ /**
+ * If we only have one row of actions, then disable the remove button for that row.
+ */
+ updateRemoveButton() {
+ this.mListBox.getItemAtIndex(0).mRemoveButton.disabled =
+ this.mListBox.getRowCount() == 1;
+ }
+
+ addRow() {
+ let listItem = document.createXULElement("richlistitem", {
+ is: "ruleaction-richlistitem",
+ });
+ listItem.classList.add("ruleaction");
+ listItem.setAttribute("onfocus", "this.storeFocus();");
+ this.mListBox.insertBefore(listItem, this.nextElementSibling);
+ this.mListBox.ensureElementIsVisible(listItem);
+
+ // Make sure the first remove button is enabled.
+ this.updateRemoveButton();
+ checkActionsReorder();
+ }
+
+ removeRow() {
+ // this.mListBox will fail after the row is removed, so save a reference.
+ let listBox = this.mListBox;
+ if (listBox.getRowCount() > 1) {
+ this.remove();
+ }
+ // Can't use 'this' as it is destroyed now.
+ listBox.getItemAtIndex(0).updateRemoveButton();
+ checkActionsReorder();
+ }
+
+ /**
+ * When this action row is focused, store its index in the parent richlistbox.
+ */
+ storeFocus() {
+ this.mListBox.setAttribute(
+ "focusedAction",
+ this.mListBox.getIndexOfItem(this)
+ );
+ }
+ }
+
+ customElements.define("ruleaction-richlistitem", MozRuleactionRichlistitem, {
+ extends: "richlistitem",
+ });
+}
diff --git a/comm/mailnews/search/content/viewLog.js b/comm/mailnews/search/content/viewLog.js
new file mode 100644
index 0000000000..6ac120b2cf
--- /dev/null
+++ b/comm/mailnews/search/content/viewLog.js
@@ -0,0 +1,38 @@
+/* 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 { MailE10SUtils } = ChromeUtils.import(
+ "resource:///modules/MailE10SUtils.jsm"
+);
+
+var gFilterList;
+var gLogFilters;
+var gLogView;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+
+function onLoad() {
+ gFilterList = window.arguments[0].filterList;
+
+ gLogFilters = document.getElementById("logFilters");
+ gLogFilters.checked = gFilterList.loggingEnabled;
+
+ gLogView = document.getElementById("logView");
+
+ // for security, disable JS
+ gLogView.browsingContext.allowJavascript = false;
+
+ MailE10SUtils.loadURI(gLogView, gFilterList.logURL);
+}
+
+function toggleLogFilters() {
+ gFilterList.loggingEnabled = gLogFilters.checked;
+}
+
+function clearLog() {
+ gFilterList.clearLog();
+
+ // reload the newly truncated file
+ gLogView.reload();
+}
diff --git a/comm/mailnews/search/content/viewLog.xhtml b/comm/mailnews/search/content/viewLog.xhtml
new file mode 100644
index 0000000000..66d2671df9
--- /dev/null
+++ b/comm/mailnews/search/content/viewLog.xhtml
@@ -0,0 +1,65 @@
+<?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 html SYSTEM "chrome://messenger/locale/viewLog.dtd">
+
+<html
+ id="viewLogWindow"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="mailnews:filterlog"
+ width="600"
+ height="375"
+ persist="screenX screenY width height"
+ scrolling="false"
+>
+ <head>
+ <title>&viewLog.title;</title>
+ <script defer="defer" src="chrome://messenger/content/viewLog.js"></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttons="accept"
+ buttonlabelaccept="&closeLog.label;"
+ buttonaccesskeyaccept="&closeLog.accesskey;"
+ >
+ <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>
+ </html:body>
+</html>