summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/search/content/searchWidgets.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/search/content/searchWidgets.js')
-rw-r--r--comm/mailnews/search/content/searchWidgets.js1779
1 files changed, 1779 insertions, 0 deletions
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",
+ });
+}