diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/search | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mailnews/search')
84 files changed, 20735 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="¬Junk.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="©Message.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> diff --git a/comm/mailnews/search/public/moz.build b/comm/mailnews/search/public/moz.build new file mode 100644 index 0000000000..39a41a1d2a --- /dev/null +++ b/comm/mailnews/search/public/moz.build @@ -0,0 +1,37 @@ +# vim: set filetype=python: +# 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/. + +XPIDL_SOURCES += [ + "nsIMsgFilter.idl", + "nsIMsgFilterCustomAction.idl", + "nsIMsgFilterHitNotify.idl", + "nsIMsgFilterList.idl", + "nsIMsgFilterPlugin.idl", + "nsIMsgFilterService.idl", + "nsIMsgOperationListener.idl", + "nsIMsgSearchAdapter.idl", + "nsIMsgSearchCustomTerm.idl", + "nsIMsgSearchNotify.idl", + "nsIMsgSearchScopeTerm.idl", + "nsIMsgSearchSession.idl", + "nsIMsgSearchTerm.idl", + "nsIMsgSearchValidityManager.idl", + "nsIMsgSearchValidityTable.idl", + "nsIMsgSearchValue.idl", + "nsIMsgTraitService.idl", + "nsMsgFilterCore.idl", + "nsMsgSearchCore.idl", +] + +XPIDL_MODULE = "msgsearch" + +EXPORTS += [ + "nsMsgBodyHandler.h", + "nsMsgResultElement.h", + "nsMsgSearchAdapter.h", + "nsMsgSearchBoolExpression.h", + "nsMsgSearchScopeTerm.h", + "nsMsgSearchTerm.h", +] diff --git a/comm/mailnews/search/public/nsIMsgFilter.idl b/comm/mailnews/search/public/nsIMsgFilter.idl new file mode 100644 index 0000000000..6cf65c774e --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgFilter.idl @@ -0,0 +1,124 @@ +/* -*- Mode: IDL; 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/. */ + + +#include "nsISupports.idl" +#include "nsMsgFilterCore.idl" + +interface nsIOutputStream; +interface nsIMsgFilterCustomAction; +interface nsIMsgFilterList; +interface nsIMsgSearchScopeTerm; +interface nsIMsgSearchValue; +interface nsIMsgSearchTerm; + +[scriptable, uuid(36d2748e-9246-44f3-bb74-46cbb0b8c23a)] +interface nsIMsgRuleAction : nsISupports { + + attribute nsMsgRuleActionType type; + + // target priority.. throws an exception if the action is not priority + attribute nsMsgPriorityValue priority; + + // target folder.. throws an exception if the action is not move to folder + attribute AUTF8String targetFolderUri; + + attribute long junkScore; + + attribute AUTF8String strValue; + + // action id if type is Custom + attribute ACString customId; + + // custom action associated with customId + // (which must be set prior to reading this attribute) + readonly attribute nsIMsgFilterCustomAction customAction; + +}; + +[scriptable, uuid(d304fcfc-b588-11e4-981c-770e1e5d46b0)] +interface nsIMsgFilter : nsISupports { + attribute nsMsgFilterTypeType filterType; + /** + * some filters are "temporary". For example, the filters we create when the user + * filters return receipts to the Sent folder. + * we don't show temporary filters in the UI + * and we don't write them to disk. + */ + attribute boolean temporary; + attribute boolean enabled; + attribute AString filterName; + attribute ACString filterDesc; + attribute ACString unparsedBuffer; //holds the entire filter if we don't know how to handle it + attribute boolean unparseable; //whether we could parse the filter or not + + attribute nsIMsgFilterList filterList; // owning filter list + + void AddTerm(in nsMsgSearchAttribValue attrib, + in nsMsgSearchOpValue op, + in nsIMsgSearchValue value, + in boolean BooleanAND, + in ACString arbitraryHeader); + + void GetTerm(in long termIndex, + out nsMsgSearchAttribValue attrib, + out nsMsgSearchOpValue op, + out nsIMsgSearchValue value, // bad! using shared structure + out boolean BooleanAND, + out ACString arbitraryHeader); + + void appendTerm(in nsIMsgSearchTerm term); + + nsIMsgSearchTerm createTerm(); + + attribute Array<nsIMsgSearchTerm> searchTerms; + + attribute nsIMsgSearchScopeTerm scope; + + boolean MatchHdr(in nsIMsgDBHdr msgHdr, in nsIMsgFolder folder, + in nsIMsgDatabase db, + in ACString headers); // null-separated list of headers + + + /* + * Report that Rule was matched and executed when filter logging is enabled. + * + * @param aFilterAction The filter rule that was invoked. + * @param aHeader The header information of the message acted on by + * the filter. + */ + void logRuleHit(in nsIMsgRuleAction aFilterAction, + in nsIMsgDBHdr aHeader); + + /* Report that filtering failed for some reason when filter logging is enabled. + * + * @param aFilterAction Filter rule that was invoked. + * @param aHeader Header of the message acted on by the filter. + * @param aRcode Error code returned by low-level routine that + * led to the filter failure. + * @param aErrmsg Error message + */ + void logRuleHitFail(in nsIMsgRuleAction aFilterAction, + in nsIMsgDBHdr aHeader, + in nsresult aRcode, + in AUTF8String aErrmsg); + + nsIMsgRuleAction createAction(); + + nsIMsgRuleAction getActionAt(in unsigned long aIndex); + + long getActionIndex(in nsIMsgRuleAction aAction); + + void appendAction(in nsIMsgRuleAction action); + + readonly attribute unsigned long actionCount; + + void clearActionList(); + + // Returns the action list in the order it will be really executed in. + readonly attribute Array<nsIMsgRuleAction> sortedActionList; + + void SaveToTextFile(in nsIOutputStream aStream); +}; diff --git a/comm/mailnews/search/public/nsIMsgFilterCustomAction.idl b/comm/mailnews/search/public/nsIMsgFilterCustomAction.idl new file mode 100644 index 0000000000..6e0f15cb09 --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgFilterCustomAction.idl @@ -0,0 +1,88 @@ +/* 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/. */ + +#include "nsMsgFilterCore.idl" + +interface nsIMsgCopyServiceListener; +interface nsIMsgWindow; +interface nsIMsgDBHdr; + +/** + * describes a custom action added to a message filter + */ +[scriptable,uuid(4699C41E-3671-436e-B6AE-4FD8106747E4)] +interface nsIMsgFilterCustomAction : nsISupports +{ + /* globally unique string to identify this filter action. + * recommended form: ExtensionName@example.com#ActionName + */ + readonly attribute ACString id; + + /* action name to display in action list. This should be localized. */ + readonly attribute AString name; + + /** + * Is this custom action valid for a particular filter type? + * + * @param type the filter type + * @param scope the search scope + * + * @return true if valid + */ + boolean isValidForType(in nsMsgFilterTypeType type, in nsMsgSearchScopeValue scope); + + /** + * After the user inputs a particular action value for the action, determine + * if that value is valid. + * + * @param actionValue The value entered. + * @param actionFolder Folder in the filter list + * @param filterType Filter Type (Manual, OfflineMail, etc.) + * + * @return errorMessage A localized message to display if invalid + * Set to null if the actionValue is valid + */ + AUTF8String validateActionValue(in AUTF8String actionValue, + in nsIMsgFolder actionFolder, + in nsMsgFilterTypeType filterType); + + /* allow duplicate actions in the same filter list? Default No. */ + attribute boolean allowDuplicates; + + /* + * The custom action itself + * + * Generally for the applyAction method, folder-based methods give correct + * results and are preferred if available. Otherwise, be careful + * that the action does correct notifications to maintain counts, and correct + * manipulations of both IMAP and local non-database storage of message + * metadata. + */ + + /** + * Apply the custom action to an array of messages + * + * @param msgHdrs array of nsIMsgDBHdr objects of messages + * @param actionValue user-set value to use in the action + * @param copyListener calling method (filterType Manual only) + * @param filterType type of filter being applied + * @param msgWindow message window + */ + + void applyAction(in Array<nsIMsgDBHdr> msgHdrs, + in AUTF8String actionValue, + in nsIMsgCopyServiceListener copyListener, + in nsMsgFilterTypeType filterType, + in nsIMsgWindow msgWindow); + + /* does this action start an async action? If so, a copy listener must + * be used to continue filter processing after the action. This only + * applies to after-the-fact (manual) filters. Call OnStopCopy when done + * using the copyListener to continue. + */ + readonly attribute boolean isAsync; + + /// Does this action need the message body? + readonly attribute boolean needsBody; +}; diff --git a/comm/mailnews/search/public/nsIMsgFilterHitNotify.idl b/comm/mailnews/search/public/nsIMsgFilterHitNotify.idl new file mode 100644 index 0000000000..c0bea47db1 --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgFilterHitNotify.idl @@ -0,0 +1,26 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIMsgFilter; +interface nsIMsgWindow; + +/////////////////////////////////////////////////////////////////////////////// +// nsIMsgFilterHitNotify is an interface designed to make evaluating filters +// easier. Clients typically open a filter list and ask the filter list to +// evaluate the filters for a particular message, and pass in an +// interface pointer to be notified of hits. The filter list will call the +// ApplyFilterHit method on the interface pointer in case of hits, along with +// the desired action and value. +// return value is used to indicate whether the +// filter list should continue trying to apply filters or not. +// +/////////////////////////////////////////////////////////////////////////////// + +[scriptable, uuid(c9f15174-1f3f-11d3-a51b-0060b0fc04b7)] +interface nsIMsgFilterHitNotify : nsISupports { + boolean applyFilterHit(in nsIMsgFilter filter, in nsIMsgWindow msgWindow); +}; diff --git a/comm/mailnews/search/public/nsIMsgFilterList.idl b/comm/mailnews/search/public/nsIMsgFilterList.idl new file mode 100644 index 0000000000..fa294d5063 --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgFilterList.idl @@ -0,0 +1,115 @@ +/* -*- 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/. */ + +#include "nsISupports.idl" +#include "nsIMsgFilterHitNotify.idl" +#include "nsMsgFilterCore.idl" + +interface nsIFile; +interface nsIOutputStream; +interface nsIMsgFilter; +interface nsIMsgFolder; + +/////////////////////////////////////////////////////////////////////////////// +// The Msg Filter List is an interface designed to make accessing filter lists +// easier. Clients typically open a filter list and either enumerate the filters, +// or add new filters, or change the order around... +// +/////////////////////////////////////////////////////////////////////////////// + +typedef long nsMsgFilterFileAttribValue; + +[scriptable, uuid(5d0ec03e-7e2f-49e9-b58a-b274c85f279e)] +interface nsIMsgFilterList : nsISupports { + + const nsMsgFilterFileAttribValue attribNone = 0; + const nsMsgFilterFileAttribValue attribVersion = 1; + const nsMsgFilterFileAttribValue attribLogging = 2; + const nsMsgFilterFileAttribValue attribName = 3; + const nsMsgFilterFileAttribValue attribEnabled = 4; + const nsMsgFilterFileAttribValue attribDescription = 5; + const nsMsgFilterFileAttribValue attribType = 6; + const nsMsgFilterFileAttribValue attribScriptFile = 7; + const nsMsgFilterFileAttribValue attribAction = 8; + const nsMsgFilterFileAttribValue attribActionValue = 9; + const nsMsgFilterFileAttribValue attribCondition = 10; + const nsMsgFilterFileAttribValue attribCustomId = 11; + + /// Unique text identifier of this filter list. + readonly attribute ACString listId; + attribute nsIMsgFolder folder; + readonly attribute short version; + readonly attribute ACString arbitraryHeaders; + readonly attribute boolean shouldDownloadAllHeaders; + readonly attribute unsigned long filterCount; + nsIMsgFilter getFilterAt(in unsigned long filterIndex); + nsIMsgFilter getFilterNamed(in AString filterName); + + void setFilterAt(in unsigned long filterIndex, in nsIMsgFilter filter); + void removeFilter(in nsIMsgFilter filter); + void removeFilterAt(in unsigned long filterIndex); + + void moveFilterAt(in unsigned long filterIndex, + in nsMsgFilterMotionValue motion); + void moveFilter(in nsIMsgFilter filter, + in nsMsgFilterMotionValue motion); + + void insertFilterAt(in unsigned long filterIndex, in nsIMsgFilter filter); + + nsIMsgFilter createFilter(in AString name); + + void saveToFile(in nsIOutputStream stream); + + void parseCondition(in nsIMsgFilter aFilter, in string condition); + // this is temporary so that we can save the filterlist to disk + // without knowing where the filters were read from initially + // (such as the filter list dialog) + attribute nsIFile defaultFile; + void saveToDefaultFile(); + + void applyFiltersToHdr(in nsMsgFilterTypeType filterType, + in nsIMsgDBHdr msgHdr, + in nsIMsgFolder folder, + in nsIMsgDatabase db, + in ACString headers, // null-separated list of headers + in nsIMsgFilterHitNotify listener, + in nsIMsgWindow msgWindow); + + // IO routines, used by filter object filing code. + void writeIntAttr(in nsMsgFilterFileAttribValue attrib, in long value, in nsIOutputStream stream); + void writeStrAttr(in nsMsgFilterFileAttribValue attrib, in string value, in nsIOutputStream stream); + void writeWstrAttr(in nsMsgFilterFileAttribValue attrib, in wstring value, in nsIOutputStream stream); + void writeBoolAttr(in nsMsgFilterFileAttribValue attrib, in boolean value, in nsIOutputStream stream); + boolean matchOrChangeFilterTarget(in AUTF8String oldUri, in AUTF8String newUri, in boolean caseInsensitive); + + /** + * Turn filter logging on or off. Turning logging off will close any + * currently-open open logfile. + */ + attribute boolean loggingEnabled; + /** + * The log will be written via logStream. This may be null if + * loggingEnabled is false or if there is some problem with the logging. + */ + attribute nsIOutputStream logStream; + readonly attribute ACString logURL; + void clearLog(); + void flushLogIfNecessary(); + /** + * Push a message to the filter log file, adding a timestamp. + * + * @param message The message text to log. + * @param filter Optional filter object that reports the message. + */ + void logFilterMessage(in AString message, [optional] in nsIMsgFilter filter); +}; + + +/* these longs are all actually of type nsMsgFilterMotionValue */ +[scriptable, uuid(d067b528-304e-11d3-a0e1-00a0c900d445)] +interface nsMsgFilterMotion : nsISupports { + const long up = 0; + const long down = 1; +}; diff --git a/comm/mailnews/search/public/nsIMsgFilterPlugin.idl b/comm/mailnews/search/public/nsIMsgFilterPlugin.idl new file mode 100644 index 0000000000..93934a364a --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgFilterPlugin.idl @@ -0,0 +1,331 @@ +/* -*- 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/. */ + +#include "nsISupports.idl" +#include "MailNewsTypes2.idl" + +interface nsIMsgWindow; +interface nsIFile; + +/** + * This interface is still very much under development, and is not yet stable. + */ + +[scriptable, uuid(e2e56690-a676-11d6-80c9-00008646b737)] +interface nsIMsgFilterPlugin : nsISupports +{ + /** + * Do any necessary cleanup: flush and close any open files, etc. + */ + void shutdown(); + + /** + * Some protocols (ie IMAP) can, as an optimization, avoid + * downloading all message header lines. If your plugin doesn't need + * any more than the minimal set, it can return false for this attribute. + */ + readonly attribute boolean shouldDownloadAllHeaders; + +}; + +/* + * These interfaces typically implement a Bayesian classifier of messages. + * + * Two sets of interfaces may be used: the older junk-only interfaces, and + * the newer trait-oriented interfaces that treat junk classification as + * one of a set of classifications to accomplish. + */ + +[scriptable, uuid(b15a0f9c-df07-4af0-9ba8-80dca68ac35d)] +interface nsIJunkMailClassificationListener : nsISupports +{ + /** + * Inform a listener of a message's classification as junk. At the end + * of a batch of classifications, signify end of batch by calling with + * null aMsgURI (other parameters are don't care) + * + * @param aMsgURI URI of the message that was classified. + * @param aClassification classification of message as UNCLASSIFIED, GOOD, + * or JUNK. + * @param aJunkPercent indicator of degree of uncertainty, with 100 being + * probably junk, and 0 probably good + */ + void onMessageClassified(in AUTF8String aMsgURI, + in nsMsgJunkStatus aClassification, + in uint32_t aJunkPercent); +}; + +[scriptable, uuid(AF247D07-72F0-482d-9EAB-5A786407AA4C)] +interface nsIMsgTraitClassificationListener : nsISupports +{ + /** + * Inform a listener of a message's match to traits. The list + * of traits being matched is in aTraits. Corresponding + * indicator of match (percent) is in aPercents. At the end + * of a batch of classifications, signify end of batch by calling with + * null aMsgURI (other parameters are don't care) + * + * @param aMsgURI URI of the message that was classified + * @param aTraits array of matched trait ids + * @param aPercents array of percent match (0 is unmatched, 100 is fully + * matched) of the trait with the corresponding array + * index in aTraits + */ + void onMessageTraitsClassified(in AUTF8String aMsgURI, + in Array<unsigned long> aTraits, + in Array<unsigned long> aPercents); +}; + +[scriptable, uuid(12667532-88D1-44a7-AD48-F73719BE5C92)] +interface nsIMsgTraitDetailListener : nsISupports +{ + /** + * Inform a listener of details of a message's match to traits. + * This returns the tokens that were used in the calculation, + * the calculated percent probability that each token matches the trait, + * and a running estimate (starting with the strongest tokens) of the + * combined total probability that a message matches the trait, when + * only tokens stronger than the current token are used. + * + * @param aMsgURI URI of the message that was classified + * @param aProTrait trait id of pro trait for the calculation + * @param tokenStrings the string for a particular token + * @param tokenPercents calculated probability that a message with that token + * matches the trait + * @param runningPercents calculated probability that the message matches the + * trait, accounting for this token and all stronger tokens. + */ + void onMessageTraitDetails(in AUTF8String aMsgUri, + in unsigned long aProTrait, + in Array<AString> tokenStrings, + in Array<unsigned long> tokenPercents, + in Array<unsigned long> runningPercents); +}; + +[scriptable, uuid(8EA5BBCA-F735-4d43-8541-D203D8E2FF2F)] +interface nsIJunkMailPlugin : nsIMsgFilterPlugin +{ + /** + * Message classifications. + */ + const nsMsgJunkStatus UNCLASSIFIED = 0; + const nsMsgJunkStatus GOOD = 1; + const nsMsgJunkStatus JUNK = 2; + + /** + * Message junk score constants. Junkscore can only be one of these two + * values (or not set). + */ + const nsMsgJunkScore IS_SPAM_SCORE = 100; // junk + const nsMsgJunkScore IS_HAM_SCORE = 0; // not junk + + /** + * Trait ids for junk analysis. These values are fixed to ensure + * backwards compatibility with existing junk-oriented classification + * code. + */ + + const unsigned long GOOD_TRAIT = 1; // good + const unsigned long JUNK_TRAIT = 2; // junk + + /** + * Given a message URI, determine what its current classification is + * according to the current training set. + */ + void classifyMessage(in AUTF8String aMsgURI, in nsIMsgWindow aMsgWindow, + in nsIJunkMailClassificationListener aListener); + + void classifyMessages(in Array<AUTF8String> aMsgURIs, + in nsIMsgWindow aMsgWindow, + in nsIJunkMailClassificationListener aListener); + + /** + * Given a message URI, evaluate its relative match to a list of + * traits according to the current training set. + * + * @param aMsgURI URI of the message to be evaluated + * @param aProTraits array of trait ids for trained messages that + * match the tested trait (for example, + * JUNK_TRAIT if testing for junk) + * @param aAntiTraits array of trait ids for trained messages that + * do not match the tested trait (for example, + * GOOD_TRAIT if testing for junk) + * @param aTraitListener trait-oriented callback listener (may be null) + * @param aMsgWindow current message window (may be null) + * @param aJunkListener junk-oriented callback listener (may be null) + */ + + void classifyTraitsInMessage( + in AUTF8String aMsgURI, + in Array<unsigned long> aProTraits, + in Array<unsigned long> aAntiTraits, + in nsIMsgTraitClassificationListener aTraitListener, + [optional] in nsIMsgWindow aMsgWindow, + [optional] in nsIJunkMailClassificationListener aJunkListener); + + /** + * Given an array of message URIs, evaluate their relative match to a + * list of traits according to the current training set. + * + * @param aMsgURIs array of URIs of the messages to be evaluated + * @param aProTraits array of trait ids for trained messages that + * match the tested trait (for example, + * JUNK_TRAIT if testing for junk) + * @param aAntiTraits array of trait ids for trained messages that + * do not match the tested trait (for example, + * GOOD_TRAIT if testing for junk) + * @param aTraitListener trait-oriented callback listener (may be null) + * @param aMsgWindow current message window (may be null) + * @param aJunkListener junk-oriented callback listener (may be null) + */ + + void classifyTraitsInMessages( + in Array<AUTF8String> aMsgURIs, + in Array<unsigned long> aProTraits, + in Array<unsigned long> aAntiTraits, + in nsIMsgTraitClassificationListener aTraitListener, + [optional] in nsIMsgWindow aMsgWindow, + [optional] in nsIJunkMailClassificationListener aJunkListener); + + /** + * Called when a user forces the classification of a message. Should + * cause the training set to be updated appropriately. + * + * @arg aMsgURI URI of the message to be classified + * @arg aOldUserClassification Was it previous manually classified + * by the user? If so, how? + * @arg aNewClassification New manual classification. + * @arg aListener Callback (may be null) + */ + void setMessageClassification( + in AUTF8String aMsgURI, in nsMsgJunkStatus aOldUserClassification, + in nsMsgJunkStatus aNewClassification, + in nsIMsgWindow aMsgWindow, + in nsIJunkMailClassificationListener aListener); + + /** + * Called when a user forces a change in the classification of a message. + * Should cause the training set to be updated appropriately. + * + * @param aMsgURI URI of the message to be classified + * @param aOldTraits array of trait IDs of the old + * message classification(s), if any + * @param aNewTraits array of trait IDs of the new + * message classification(s), if any + * @param aTraitListener trait-oriented listener (may be null) + * @param aMsgWindow current message window (may be null) + * @param aJunkListener junk-oriented listener (may be null) + */ + void setMsgTraitClassification( + in AUTF8String aMsgURI, + in Array<unsigned long> aOldTraits, + in Array<unsigned long> aNewTraits, + [optional] in nsIMsgTraitClassificationListener aTraitListener, + [optional] in nsIMsgWindow aMsgWindow, + [optional] in nsIJunkMailClassificationListener aJunkListener); + + readonly attribute boolean userHasClassified; + + /** Removes the training file and clears out any in memory training tokens. + User must retrain after doing this. + **/ + void resetTrainingData(); + + /** + * Given a message URI, return a list of tokens and their contribution to + * the analysis of a message's match to a trait according to the + * current training set. + * + * @param aMsgURI URI of the message to be evaluated + * @param aProTrait trait id for trained messages that match the + * tested trait (for example, JUNK_TRAIT if testing + * for junk) + * @param aAntiTrait trait id for trained messages that do not match + * the tested trait (for example, GOOD_TRAIT + * if testing for junk) + * @param aListener callback listener for results + * @param aMsgWindow current message window (may be null) + */ + void detailMessage( + in AUTF8String aMsgURI, + in unsigned long aProTrait, + in unsigned long aAntiTrait, + in nsIMsgTraitDetailListener aListener, + [optional] in nsIMsgWindow aMsgWindow); + +}; + +/** + * The nsIMsgCorpus interface manages a corpus of mail data used for + * statistical analysis of messages. + */ +[scriptable, uuid(70BAD26F-DFD4-41bd-8FAB-4C09B9C1E845)] +interface nsIMsgCorpus : nsISupports +{ + /** + * Clear the corpus data for a trait id. + * + * @param aTrait trait id + */ + void clearTrait(in unsigned long aTrait); + + /** + * Update corpus data from a file. + * Uses the parallel arrays aFromTraits and aToTraits. These arrays allow + * conversion of the trait id stored in the file (which may be originated + * externally) to the trait id used in the local corpus (which is defined + * locally using nsIMsgTraitService, and mapped by that interface to a + * globally unique trait id string). + * + * @param aFile the file with the data, in the format: + * + * Format of the trait file for version 1: + * [0xFCA93601] (the 01 is the version) + * for each trait to write: + * [id of trait to write] (0 means end of list) + * [number of messages per trait] + * for each token with non-zero count + * [count] + * [length of word]word + * + * @param aIsAdd should the data be added, or removed? True if + * adding, false if removing. + * + * @param aFromTraits array of trait ids used in aFile. If aFile contains + * trait ids that are not in this array, they are not + * remapped, but assumed to be local trait ids. + * + * @param aToTraits array of trait ids, corresponding to elements of + * aFromTraits, that represent the local trait ids to + * be used in storing data from aFile into the local corpus. + */ + void updateData(in nsIFile aFile, in boolean aIsAdd, + [optional] in Array<unsigned long> aFromTraits, + [optional] in Array<unsigned long> aToTraits); + + /** + * Get the corpus count for a token as a string. + * + * @param aWord string of characters representing the token + * @param aTrait trait id + * + * @return count of that token in the corpus + * + */ + unsigned long getTokenCount(in AUTF8String aWord, in unsigned long aTrait); + + /** + * Gives information on token and message count information in the + * training data corpus. + * + * @param aTrait trait id (may be null) + * @param aMessageCount count of messages that have been trained with aTrait + * + * @return token count for all traits + */ + + unsigned long corpusCounts(in unsigned long aTrait, out unsigned long aMessageCount); +}; diff --git a/comm/mailnews/search/public/nsIMsgFilterService.idl b/comm/mailnews/search/public/nsIMsgFilterService.idl new file mode 100644 index 0000000000..439dd40f93 --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgFilterService.idl @@ -0,0 +1,102 @@ +/* -*- Mode: IDL; 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/. */ + +#include "nsISupports.idl" +#include "nsMsgFilterCore.idl" + +interface nsIMsgFilterList; +interface nsIMsgWindow; +interface nsIMsgFilterCustomAction; +interface nsIFile; +interface nsIMsgFolder; +interface nsIMsgSearchCustomTerm; +interface nsIMsgOperationListener; + +[scriptable, uuid(78a74023-1692-4567-8d72-9ca58fbbd427)] +interface nsIMsgFilterService : nsISupports { + + nsIMsgFilterList OpenFilterList(in nsIFile filterFile, in nsIMsgFolder rootFolder, in nsIMsgWindow msgWindow); + void CloseFilterList(in nsIMsgFilterList filterList); + + void SaveFilterList(in nsIMsgFilterList filterList, + in nsIFile filterFile); + + void CancelFilterList(in nsIMsgFilterList filterList); + nsIMsgFilterList getTempFilterList(in nsIMsgFolder aFolder); + void applyFiltersToFolders(in nsIMsgFilterList aFilterList, + in Array<nsIMsgFolder> aFolders, + in nsIMsgWindow aMsgWindow, + [optional] in nsIMsgOperationListener aCallback); + + /** + * Apply filters to a specific list of messages in a folder. + * @param aFilterType The type of filter to match against + * @param aMsgHdrList The list of message headers (nsIMsgDBHdr objects) + * @param aFolder The folder the messages belong to + * @param aMsgWindow A UI window for attaching progress/dialogs + * @param aCallback A listener that gets notified of any filtering error + */ + void applyFilters(in nsMsgFilterTypeType aFilterType, + in Array<nsIMsgDBHdr> aMsgHdrList, + in nsIMsgFolder aFolder, + in nsIMsgWindow aMsgWindow, + [optional] in nsIMsgOperationListener aCallback); + + /** + * Add a custom filter action. + * + * @param aAction the custom action to add + */ + void addCustomAction(in nsIMsgFilterCustomAction aAction); + + /** + * get the list of custom actions + * + * @return an array of nsIMsgFilterCustomAction objects + */ + Array<nsIMsgFilterCustomAction> getCustomActions(); + + /** + * Lookup a custom action given its id. + * + * @param id unique identifier for a particular custom action + * + * @return the custom action, or null if not found + */ + nsIMsgFilterCustomAction getCustomAction(in ACString id); + + /** + * Add a custom search term. + * + * @param aTerm the custom term to add + */ + void addCustomTerm(in nsIMsgSearchCustomTerm aTerm); + + /** + * get the list of custom search terms + * + * @return an array of nsIMsgSearchCustomTerm objects + */ + Array<nsIMsgSearchCustomTerm> getCustomTerms(); + + /** + * Lookup a custom search term given its id. + * + * @param id unique identifier for a particular custom search term + * + * @return the custom search term, or null if not found + */ + nsIMsgSearchCustomTerm getCustomTerm(in ACString id); + + /** + * Translate the filter type flag into human readable type names. + * In case of multiple flag they are delimited by '&'. + * + * @param filterType nsMsgFilterType flags of filter type + * + * @return A string describing the filter type. + */ + ACString filterTypeName(in nsMsgFilterTypeType filterType); +}; diff --git a/comm/mailnews/search/public/nsIMsgOperationListener.idl b/comm/mailnews/search/public/nsIMsgOperationListener.idl new file mode 100644 index 0000000000..30751cd5c3 --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgOperationListener.idl @@ -0,0 +1,17 @@ +/* -*- Mode: IDL; 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/. */ + +#include "nsISupports.idl" + +// Listener used to notify when an operation has completed. +[scriptable, uuid(bdaef6ff-0909-435b-8fcd-76525dd2364c)] +interface nsIMsgOperationListener : nsISupports { + /** + * Called when the operation stops (possibly with errors) + * + * @param aStatus Success or failure of the operation + */ + void onStopOperation(in nsresult aStatus); +}; diff --git a/comm/mailnews/search/public/nsIMsgSearchAdapter.idl b/comm/mailnews/search/public/nsIMsgSearchAdapter.idl new file mode 100644 index 0000000000..9784fdda79 --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgSearchAdapter.idl @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + +#include "nsISupports.idl" +#include "nsIMsgSearchScopeTerm.idl" + +[ptr] native nsMsgResultElement(nsMsgResultElement); + +%{C++ +class nsMsgResultElement; +%} + +[scriptable, uuid(0b09078b-e0cd-440a-afee-01f45808ee74)] +interface nsIMsgSearchAdapter : nsISupports { + void ValidateTerms(); + void Search(out boolean done); + void SendUrl(); + void CurrentUrlDone(in nsresult exitCode); + + void AddHit(in nsMsgKey key); + void AddResultElement(in nsIMsgDBHdr aHdr); + + [noscript] void OpenResultElement(in nsMsgResultElement element); + [noscript] void ModifyResultElement(in nsMsgResultElement element, + in nsMsgSearchValue value); + + readonly attribute string encoding; + + [noscript] nsIMsgFolder FindTargetFolder([const] in nsMsgResultElement + element); + void Abort(); + void getSearchCharsets(out AString srcCharset, out AString destCharset); + /* + * Clear the saved scope reference. This is used when deleting scope, which is not + * reference counted in nsMsgSearchSession + */ + void clearScope(); +}; diff --git a/comm/mailnews/search/public/nsIMsgSearchCustomTerm.idl b/comm/mailnews/search/public/nsIMsgSearchCustomTerm.idl new file mode 100644 index 0000000000..b2e3c024cd --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgSearchCustomTerm.idl @@ -0,0 +1,75 @@ +/* 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/. */ + +#include "nsMsgSearchCore.idl" + +/** + * describes a custom term added to a message search or filter + */ +[scriptable,uuid(925DB5AA-21AF-494c-8652-984BC7BAD13A)] +interface nsIMsgSearchCustomTerm : nsISupports +{ + /** + * globally unique string to identify this search term. + * recommended form: ExtensionName@example.com#TermName + * Commas and quotes are not allowed, the id must not + * parse to an integer, and names of standard search + * attributes in SearchAttribEntryTable in nsMsgSearchTerm.cpp + * are not allowed. + */ + readonly attribute ACString id; + + /// name to display in term list. This should be localized. */ + readonly attribute AString name; + + /// Does this term need the message body? + readonly attribute boolean needsBody; + + /** + * Is this custom term enabled? + * + * @param scope search scope (nsMsgSearchScope) + * @param op search operator (nsMsgSearchOp). If null, determine + * if term is available for any operator. + * + * @return true if enabled + */ + boolean getEnabled(in nsMsgSearchScopeValue scope, + in nsMsgSearchOpValue op); + + /** + * Is this custom term available? + * + * @param scope search scope (nsMsgSearchScope) + * @param op search operator (nsMsgSearchOp). If null, determine + * if term is available for any operator. + * + * @return true if available + */ + boolean getAvailable(in nsMsgSearchScopeValue scope, + in nsMsgSearchOpValue op); + + /** + * List the valid operators for this term. + * + * @param scope search scope (nsMsgSearchScope) + * + * @return array of operators + */ + Array<nsMsgSearchOpValue> getAvailableOperators(in nsMsgSearchScopeValue scope); + + /** + * Apply the custom search term to a message + * + * @param msgHdr header database reference representing the message + * @param searchValue user-set value to use in the search + * @param searchOp search operator (Contains, IsHigherThan, etc.) + * + * @return true if the term matches the message, else false + */ + + boolean match(in nsIMsgDBHdr msgHdr, + in AUTF8String searchValue, + in nsMsgSearchOpValue searchOp); +}; diff --git a/comm/mailnews/search/public/nsIMsgSearchNotify.idl b/comm/mailnews/search/public/nsIMsgSearchNotify.idl new file mode 100644 index 0000000000..1e3493ad83 --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgSearchNotify.idl @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIMsgDBHdr; +interface nsIMsgSearchSession; +interface nsIMsgFolder; + +// when a search is run, this interface is passed in as a listener +// on the search. +[scriptable, uuid(ca37784d-352b-4c39-8ccb-0abc1a93f681)] +interface nsIMsgSearchNotify : nsISupports +{ + void onSearchHit(in nsIMsgDBHdr header, in nsIMsgFolder folder); + + // notification that a search has finished. + void onSearchDone(in nsresult status); + /* + * until we can encode searches with a URI, this will be an + * out-of-bound way to connect a set of search terms to a datasource + */ + + /* + * called when a new search begins + */ + void onNewSearch(); +}; diff --git a/comm/mailnews/search/public/nsIMsgSearchScopeTerm.idl b/comm/mailnews/search/public/nsIMsgSearchScopeTerm.idl new file mode 100644 index 0000000000..63a130d9ea --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgSearchScopeTerm.idl @@ -0,0 +1,19 @@ +/* -*- Mode: IDL; 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/. */ + +#include "nsIMsgSearchSession.idl" + +interface nsIMsgFolder; +interface nsIMsgDBHdr; +interface nsILineInputStream; +interface nsIInputStream; + +[scriptable, uuid(934672c3-9b8f-488a-935d-87b4023fa0be)] +interface nsIMsgSearchScopeTerm : nsISupports { + nsIInputStream getInputStream(in nsIMsgDBHdr aHdr); + void closeInputStream(); + readonly attribute nsIMsgFolder folder; + readonly attribute nsIMsgSearchSession searchSession; +}; diff --git a/comm/mailnews/search/public/nsIMsgSearchSession.idl b/comm/mailnews/search/public/nsIMsgSearchSession.idl new file mode 100644 index 0000000000..024ce829b2 --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgSearchSession.idl @@ -0,0 +1,130 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsIMsgSearchValue.idl" + +interface nsIMsgSearchAdapter; +interface nsIMsgSearchTerm; +interface nsIMsgSearchNotify; +interface nsIMsgDatabase; +interface nsIMsgWindow; + +////////////////////////////////////////////////////////////////////////////// +// The Msg Search Session is an interface designed to make constructing +// searches easier. Clients typically build up search terms, and then run +// the search +////////////////////////////////////////////////////////////////////////////// + +[scriptable, uuid(1ed69bbf-7983-4602-9a9b-2f2263a78878)] +interface nsIMsgSearchSession : nsISupports { + +/** + * add a search term to the search session + * + * @param attrib search attribute (e.g. nsMsgSearchAttrib::Subject) + * @param op search operator (e.g. nsMsgSearchOp::Contains) + * @param value search value (e.g. "Dogbert", see nsIMsgSearchValue) + * @param BooleanAND set to true if associated boolean operator is AND + * @param customString if attrib > nsMsgSearchAttrib::OtherHeader, + * a user defined arbitrary header + * if attrib == nsMsgSearchAttrib::Custom, the custom id + * otherwise ignored + */ + void addSearchTerm(in nsMsgSearchAttribValue attrib, + in nsMsgSearchOpValue op, + in nsIMsgSearchValue value, + in boolean BooleanAND, + in string customString); + + attribute Array<nsIMsgSearchTerm> searchTerms; + + nsIMsgSearchTerm createTerm(); + void appendTerm(in nsIMsgSearchTerm term); + + /** + * @name Search notification flags + * These flags determine which notifications will be sent. + * @{ + */ + /// search started notification + const long onNewSearch = 0x1; + + /// search finished notification + const long onSearchDone = 0x2; + + /// search hit notification + const long onSearchHit = 0x4; + + const long allNotifications = 0x7; + /** @} */ + + /** + * Add a listener to get notified of search starts, stops, and hits. + * + * @param aListener listener + * @param aNotifyFlags which notifications to send. Defaults to all + */ + void registerListener(in nsIMsgSearchNotify aListener, + [optional] in long aNotifyFlags); + void unregisterListener(in nsIMsgSearchNotify listener); + + readonly attribute unsigned long numSearchTerms; + + readonly attribute nsIMsgSearchAdapter runningAdapter; + + void getNthSearchTerm(in long whichTerm, + in nsMsgSearchAttribValue attrib, + in nsMsgSearchOpValue op, + in nsIMsgSearchValue value); // wrong, should be out + + long countSearchScopes(); + + void getNthSearchScope(in long which,out nsMsgSearchScopeValue scopeId, out nsIMsgFolder folder); + + /* add a scope (e.g. a mail folder) to the search */ + void addScopeTerm(in nsMsgSearchScopeValue scope, + in nsIMsgFolder folder); + + void addDirectoryScopeTerm(in nsMsgSearchScopeValue scope); + + void clearScopes(); + + /* Call this function every time the scope changes! It informs the FE if + the current scope support custom header use. FEs should not display the + custom header dialog if custom headers are not supported */ + [noscript] boolean ScopeUsesCustomHeaders(in nsMsgSearchScopeValue scope, + /* could be a folder or server based on scope */ + in voidPtr selection, + in boolean forFilters); + + /* use this to determine if your attribute is a string attrib */ + boolean IsStringAttribute(in nsMsgSearchAttribValue attrib); + + /* add all scopes of a given type to the search */ + void AddAllScopes(in nsMsgSearchScopeValue attrib); + + void search(in nsIMsgWindow aWindow); + void interruptSearch(); + + // these two methods are used when the search session is using + // a timer to do local search, and the search adapter needs + // to run a url (e.g., to reparse a local folder) and wants to + // pause the timer while running the url. This will fail if the + // current adapter is not using a timer. + void pauseSearch(); + void resumeSearch(); + + boolean MatchHdr(in nsIMsgDBHdr aMsgHdr, in nsIMsgDatabase aDatabase); + + void addSearchHit(in nsIMsgDBHdr header, in nsIMsgFolder folder); + + readonly attribute long numResults; + attribute nsIMsgWindow window; + + /* these longs are all actually of type nsMsgSearchBooleanOp */ + const long BooleanOR=0; + const long BooleanAND=1; +}; diff --git a/comm/mailnews/search/public/nsIMsgSearchTerm.idl b/comm/mailnews/search/public/nsIMsgSearchTerm.idl new file mode 100644 index 0000000000..5724ba1c1a --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgSearchTerm.idl @@ -0,0 +1,153 @@ +/* -*- Mode: IDL; 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/. */ + +#include "nsISupports.idl" +#include "nsMsgSearchCore.idl" +#include "nsIMsgSearchValue.idl" + +interface nsIMsgDBHdr; +interface nsIMsgDatabase; +interface nsIMsgSearchScopeTerm; + +[scriptable, uuid(705a2b5a-5efc-495c-897a-bef1161cd3c0)] +interface nsIMsgSearchTerm : nsISupports { + attribute nsMsgSearchAttribValue attrib; + attribute nsMsgSearchOpValue op; + attribute nsIMsgSearchValue value; + + attribute boolean booleanAnd; + attribute ACString arbitraryHeader; + /** + * Not to be confused with arbitraryHeader, which is a header in the + * rfc822 message. This is a property of the nsIMsgDBHdr, and may have + * nothing to do the message headers, e.g., gloda-id. + * value.str will be compared with nsIMsgHdr::GetProperty(hdrProperty). + */ + attribute ACString hdrProperty; + + /// identifier for a custom id used for this term, if any. + attribute ACString customId; + + attribute boolean beginsGrouping; + attribute boolean endsGrouping; + + /** + * Match the value against one of the emails found in the incoming + * 2047-encoded string. + */ + boolean matchRfc822String(in ACString aString, in string charset); + /** + * Match the current header value against the incoming 2047-encoded string. + * + * This method will first apply the nsIMimeConverter decoding to the string + * (using the supplied parameters) and will then match the value against the + * decoded result. + */ + boolean matchRfc2047String(in ACString aString, in string charset, in boolean charsetOverride); + boolean matchDate(in PRTime aTime); + boolean matchStatus(in unsigned long aStatus); + boolean matchPriority(in nsMsgPriorityValue priority); + boolean matchAge(in PRTime days); + boolean matchSize(in unsigned long size); + boolean matchJunkStatus(in string aJunkScore); + /* + * Test search term match for junkpercent + * + * @param aJunkPercent junkpercent for message (0-100, 100 is junk) + * @return true if matches + */ + boolean matchJunkPercent(in unsigned long aJunkPercent); + /* + * Test search term match for junkscoreorigin + * @param aJunkScoreOrigin Who set junk score? Possible values: + * plugin filter imapflag user whitelist + * @return true if matches + */ + boolean matchJunkScoreOrigin(in string aJunkScoreOrigin); + + /** + * Test if the body of the passed in message matches "this" search term. + * @param aScopeTerm scope of search + * @param aOffset offset of message in message store. + * @param aLength length of message. + * @param aCharset folder charset. + * @param aMsg db msg hdr of message to match. + * @param aDB db containing msg header. + */ + boolean matchBody(in nsIMsgSearchScopeTerm aScopeTerm, + in unsigned long long aOffset, + in unsigned long aLength, + in string aCharset, + in nsIMsgDBHdr aMsg, + in nsIMsgDatabase aDb); + + /** + * Test if the arbitrary header specified by this search term + * matches the corresponding header in the passed in message. + * + * @param aScopeTerm scope of search + * @param aLength length of message + * @param aCharset The charset to apply to un-labeled non-UTF-8 data. + * @param aCharsetOverride If true, aCharset is used instead of any + * charset labeling other than UTF-8. + * @param aMsg The nsIMsgDBHdr of the message + * @param aDb The message database containing aMsg + * @param aHeaders A null-separated list of message headers. + * @param aForFilters Whether this is a filter or a search operation. + */ + boolean matchArbitraryHeader(in nsIMsgSearchScopeTerm aScopeTerm, + in unsigned long aLength, + in string aCharset, + in boolean aCharsetOverride, + in nsIMsgDBHdr aMsg, + in nsIMsgDatabase aDb, + in ACString aHeaders, + in boolean aForFilters); + + /** + * Compares value.str with nsIMsgHdr::GetProperty(hdrProperty). + * @param msg msg to match db hdr property of. + * + * @returns true if msg matches property, false otherwise. + */ + boolean matchHdrProperty(in nsIMsgDBHdr msg); + + /** + * Compares value.status with nsIMsgHdr::GetUint32Property(hdrProperty). + * @param msg msg to match db hdr property of. + * + * @returns true if msg matches property, false otherwise. + */ + boolean matchUint32HdrProperty(in nsIMsgDBHdr msg); + + /** + * Compares value.status with the folder flags of the msg's folder. + * @param msg msgHdr whose folder's flag we want to compare. + * + * @returns true if folder's flags match value.status, false otherwise. + */ + boolean matchFolderFlag(in nsIMsgDBHdr msg); + + readonly attribute boolean matchAllBeforeDeciding; + + readonly attribute ACString termAsString; + boolean matchKeyword(in ACString keyword); // used for tag searches + attribute boolean matchAll; + /** + * Does the message match the custom search term? + * + * @param msg message database object representing the message + * + * @return true if message matches + */ + boolean matchCustom(in nsIMsgDBHdr msg); + + /** + * Returns a nsMsgSearchAttribValue value corresponding to a field string from + * the nsMsgSearchTerm.cpp::SearchAttribEntryTable table. + * Does not handle custom attributes yet. + */ + nsMsgSearchAttribValue getAttributeFromString(in string aAttribName); +}; diff --git a/comm/mailnews/search/public/nsIMsgSearchValidityManager.idl b/comm/mailnews/search/public/nsIMsgSearchValidityManager.idl new file mode 100644 index 0000000000..dbc7958bd8 --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgSearchValidityManager.idl @@ -0,0 +1,26 @@ +/* -*- Mode: IDL; 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/. */ + +#include "nsISupports.idl" +#include "nsIMsgSearchValidityTable.idl" + +typedef long nsMsgSearchValidityScope; + +[scriptable, uuid(6A352055-DE6E-49d2-A256-89E0B9EC405E)] +interface nsIMsgSearchValidityManager : nsISupports { + nsIMsgSearchValidityTable getTable(in nsMsgSearchValidityScope scope); + + /** + * Given a search attribute (which is an internal numerical id), return + * the string name that you can use as a key to look up the localized + * string in the search-attributes.properties file. + * + * @param aSearchAttribute attribute type from interface nsMsgSearchAttrib + * + * @return localization-friendly string representation + * of the attribute + */ + AString getAttributeProperty(in nsMsgSearchAttribValue aSearchAttribute); +}; diff --git a/comm/mailnews/search/public/nsIMsgSearchValidityTable.idl b/comm/mailnews/search/public/nsIMsgSearchValidityTable.idl new file mode 100644 index 0000000000..75fd4efd51 --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgSearchValidityTable.idl @@ -0,0 +1,32 @@ +/* -*- Mode: IDL; 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/. */ + +#include "nsISupports.idl" +#include "nsMsgSearchCore.idl" + +[scriptable, uuid(b07f1cb6-fae9-4d92-9edb-03f9ad249c66)] +interface nsIMsgSearchValidityTable : nsISupports { + + void setAvailable(in nsMsgSearchAttribValue attrib, + in nsMsgSearchOpValue op, in boolean active); + void setEnabled(in nsMsgSearchAttribValue attrib, + in nsMsgSearchOpValue op, in boolean enabled); + void setValidButNotShown(in nsMsgSearchAttribValue attrib, + in nsMsgSearchOpValue op, in boolean valid); + + boolean getAvailable(in nsMsgSearchAttribValue attrib, + in nsMsgSearchOpValue op); + boolean getEnabled(in nsMsgSearchAttribValue attrib, + in nsMsgSearchOpValue op); + boolean getValidButNotShown(in nsMsgSearchAttribValue attrib, + in nsMsgSearchOpValue op); + + readonly attribute long numAvailAttribs; + + Array<nsMsgSearchAttribValue> getAvailableAttributes(); + Array<nsMsgSearchOpValue> getAvailableOperators(in nsMsgSearchAttribValue attrib); + + void setDefaultAttrib(in nsMsgSearchAttribValue defaultAttrib); +}; diff --git a/comm/mailnews/search/public/nsIMsgSearchValue.idl b/comm/mailnews/search/public/nsIMsgSearchValue.idl new file mode 100644 index 0000000000..6a4cec0b28 --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgSearchValue.idl @@ -0,0 +1,35 @@ +/* -*- 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/. */ + +#include "nsMsgSearchCore.idl" + +interface nsIMsgFolder; + +[scriptable, uuid(783758a0-cdb5-11dc-95ff-0800200c9a66)] +interface nsIMsgSearchValue : nsISupports { + // type of object + attribute nsMsgSearchAttribValue attrib; + + // accessing these will throw an exception if the above + // attribute does not match the type! + attribute AString str; + attribute nsMsgPriorityValue priority; + attribute PRTime date; + // see nsMsgMessageFlags.idl and nsMsgFolderFlags.idl + attribute unsigned long status; + attribute unsigned long size; + attribute nsMsgKey msgKey; + attribute long age; // in days + attribute nsIMsgFolder folder; + attribute nsMsgJunkStatus junkStatus; + /* + * junkPercent is set by the message filter plugin, and is approximately + * proportional to the probability that a message is junk. + * (range 0-100, 100 is junk) + */ + attribute unsigned long junkPercent; + + AString toString(); +}; diff --git a/comm/mailnews/search/public/nsIMsgTraitService.idl b/comm/mailnews/search/public/nsIMsgTraitService.idl new file mode 100644 index 0000000000..23ade21d8d --- /dev/null +++ b/comm/mailnews/search/public/nsIMsgTraitService.idl @@ -0,0 +1,182 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + + /** + * This interface provides management of traits that are used to categorize + * messages. A trait is some characteristic of a message, such as being "junk" + * or "personal", that may be discoverable by analysis of the message. + * + * Traits are described by a universal identifier "id" as a string, as well + * as a local integer identifier "index". One purpose of this service is to + * provide the mapping between those forms. + * + * Recommended (but not required) format for id: + * "extensionName@example.org#traitName" + */ + +#include "nsISupports.idl" + +[scriptable, uuid(2CB15FB0-A912-40d3-8882-F2765C75655F)] +interface nsIMsgTraitService : nsISupports +{ + /** + * the highest ever index for a registered trait. The first trait is 1, + * == 0 means no traits are defined + */ + readonly attribute long lastIndex; + + /** + * Register a trait. May be called multiple times, but subsequent + * calls do not register the trait + * + * @param id the trait universal identifier + * + * @return the internal index for the registered trait if newly + * registered, else 0 + */ + unsigned long registerTrait(in ACString id); + + /** + * Unregister a trait. + * + * @param id the trait universal identifier + */ + void unRegisterTrait(in ACString id); + + /** + * is a trait registered? + * + * @param id the trait universal identifier + * + * @return true if registered + */ + boolean isRegistered(in ACString id); + + /** + * set the trait name, which is an optional short description of the trait + * + * @param id the trait universal identifier + * @param name description of the trait. + */ + void setName(in ACString id, in ACString name); + + /** + * get the trait name, which is an optional short description of the trait + * + * @param id the trait universal identifier + * + * @return description of the trait + */ + ACString getName(in ACString id); + + /** + * get the internal index number for the trait. + * + * @param id the trait universal identifier + * + * @return internal index number for the trait + */ + unsigned long getIndex(in ACString id); + + /** + * get the trait universal identifier for an internal trait index + * + * @param index the internal identifier for the trait + * + * @return trait universal identifier + */ + ACString getId(in unsigned long index); + + /** + * enable the trait for analysis. Each enabled trait will be analyzed by + * the bayesian code. The enabled trait is the "pro" trait that represents + * messages matching the trait. Each enabled trait also needs a corresponding + * anti trait defined, which represents messages that do not match the trait. + * The anti trait does not need to be enabled + * + * @param id the trait universal identifier + * @param enabled should this trait be processed by the bayesian analyzer? + */ + void setEnabled(in ACString id, in boolean enabled); + + /** + * Should this trait be processed by the bayes analyzer? + * + * @param id the trait universal identifier + * + * @return true if this is a "pro" trait to process + */ + boolean getEnabled(in ACString id); + + /** + * set the anti trait, which indicates messages that have been marked as + * NOT matching a particular trait. + * + * @param id the trait universal identifier + * @param antiId trait id for messages marked as not matching the trait + */ + void setAntiId(in ACString id, in ACString antiId); + + /** + * get the id of traits that do not match a particular trait + * + * @param id the trait universal identifier for a "pro" trait + * + * @return universal trait identifier for an "anti" trait that does not + * match the "pro" trait messages + */ + ACString getAntiId(in ACString id); + + /** + * Get an array of "pro" traits to be analyzed by the bayesian code. This is + * a "pro" trait of messages that match the trait. + * Only enabled traits are returned. + * This should return the same number of indices as the corresponding call to + * getEnabledAntiIndices(). + * + * @return an array of trait internal indices for "pro" trait to analyze + */ + Array<unsigned long> getEnabledProIndices(); + + /** + * Get an array of "anti" traits to be analyzed by the bayesian code. This is + * a "anti" trait of messages that do not match the trait. + * Only enabled traits are returned. + * This should return the same number of indices as the corresponding call to + * getEnabledProIndices(). + * + * @return an array of trait internal indices for "anti" trait to analyze + */ + Array<unsigned long> getEnabledAntiIndices(); + + /** + * Add a trait as an alias of another trait. An alias is a trait whose + * counts will be combined with the aliased trait. This allows multiple sets + * of corpus data to be used to provide information on a single message + * characteristic, while allowing each individual set of corpus data to + * retain its own identity. + * + * @param aTraitIndex the internal identifier for the aliased trait + * @param aTraitAlias the internal identifier for the alias to add + */ + void addAlias(in unsigned long aTraitIndex, in unsigned long aTraitAlias); + + /** + * Removes a trait as an alias of another trait. + * + * @param aTraitIndex the internal identifier for the aliased trait + * @param aTraitAlias the internal identifier for the alias to remove + */ + void removeAlias(in unsigned long aTraitIndex, in unsigned long aTraitAlias); + + /** + * Get an array of trait aliases for a trait index, if any + * + * @param aTraitIndex the internal identifier for the aliased trait + * + * @return an array of internal identifiers for aliases + */ + Array<unsigned long> getAliases(in unsigned long aTraitIndex); + +}; diff --git a/comm/mailnews/search/public/nsMsgBodyHandler.h b/comm/mailnews/search/public/nsMsgBodyHandler.h new file mode 100644 index 0000000000..1252e2c20b --- /dev/null +++ b/comm/mailnews/search/public/nsMsgBodyHandler.h @@ -0,0 +1,110 @@ +/* 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/. */ + +#ifndef __nsMsgBodyHandler_h +#define __nsMsgBodyHandler_h + +#include "nsIMsgSearchScopeTerm.h" +#include "nsILineInputStream.h" +#include "nsIMsgDatabase.h" + +//--------------------------------------------------------------------------- +// nsMsgBodyHandler: used to retrieve lines from POP and IMAP offline messages. +// This is a helper class used by nsMsgSearchTerm::MatchBody +//--------------------------------------------------------------------------- +class nsMsgBodyHandler { + public: + nsMsgBodyHandler(nsIMsgSearchScopeTerm*, uint32_t length, nsIMsgDBHdr* msg, + nsIMsgDatabase* db); + + // we can also create a body handler when doing arbitrary header + // filtering...we need the list of headers and the header size as well + // if we are doing filtering...if ForFilters is false, headers and + // headersSize is ignored!!! + nsMsgBodyHandler(nsIMsgSearchScopeTerm*, uint32_t length, nsIMsgDBHdr* msg, + nsIMsgDatabase* db, + const char* headers /* NULL terminated list of headers */, + uint32_t headersSize, bool ForFilters); + + virtual ~nsMsgBodyHandler(); + + // Returns next message line in buf and the applicable charset, if found. + // The return value is the length of 'buf' or -1 for EOF. + int32_t GetNextLine(nsCString& buf, nsCString& charset); + bool IsQP() { return m_partIsQP; } + + // Transformations + void SetStripHeaders(bool strip) { m_stripHeaders = strip; } + + protected: + void Initialize(); // common initialization code + + // filter related methods. For filtering we always use the headers + // list instead of the database... + bool m_Filtering; + int32_t GetNextFilterLine(nsCString& buf); + // pointer into the headers list in the original message hdr db... + const char* m_headers; + uint32_t m_headersSize; + uint32_t m_headerBytesRead; + + // local / POP related methods + void OpenLocalFolder(); + + // goes through the mail folder + int32_t GetNextLocalLine(nsCString& buf); + + nsIMsgSearchScopeTerm* m_scope; + nsCOMPtr<nsILineInputStream> m_fileLineStream; + nsCOMPtr<nsIFile> m_localFile; + + /** + * The number of lines in the message. If |m_lineCountInBodyLines| then this + * is the number of body lines, otherwise this is the entire number of lines + * in the message. This is important so we know when to stop reading the file + * without accidentally reading part of the next message. + */ + uint32_t m_numLocalLines; + /** + * When true, |m_numLocalLines| is the number of body lines in the message, + * when false it is the entire number of lines in the message. + * + * When a message is an offline IMAP or news message, then the number of lines + * will be the entire number of lines, so this should be false. When the + * message is a local message, the number of lines will be the number of body + * lines. + */ + bool m_lineCountInBodyLines; + + // Offline IMAP related methods & state + + nsCOMPtr<nsIMsgDBHdr> m_msgHdr; + nsCOMPtr<nsIMsgDatabase> m_db; + + // Transformations + // With the exception of m_isMultipart, these all apply to the various parts + bool m_stripHeaders; // true if we're supposed to strip of message headers + bool m_pastMsgHeaders; // true if we've already skipped over the message + // headers + bool m_pastPartHeaders; // true if we've already skipped over the part + // headers + bool m_partIsQP; // true if the Content-Transfer-Encoding header claims + // quoted-printable + bool m_partIsHtml; // true if the Content-type header claims text/html + bool m_base64part; // true if the current part is in base64 + bool m_isMultipart; // true if the message is a multipart/* message + bool m_partIsText; // true if the current part is text/* + bool m_inMessageAttachment; // true if current part is message/* + + nsTArray<nsCString> m_boundaries; // The boundary strings to look for + nsCString m_partCharset; // The charset found in the part + + // See implementation for comments + int32_t ApplyTransformations(const nsCString& line, int32_t length, + bool& returnThisLine, nsCString& buf); + void SniffPossibleMIMEHeader(const nsCString& line); + static void StripHtml(nsCString& buf); + static void Base64Decode(nsCString& buf); +}; +#endif diff --git a/comm/mailnews/search/public/nsMsgFilterCore.idl b/comm/mailnews/search/public/nsMsgFilterCore.idl new file mode 100644 index 0000000000..42adc5e730 --- /dev/null +++ b/comm/mailnews/search/public/nsMsgFilterCore.idl @@ -0,0 +1,62 @@ +/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgSearchCore.idl" + +typedef long nsMsgFilterTypeType; + +[scriptable,uuid(b963a9c6-3a75-4d91-9f79-7186418d4d2d)] +interface nsMsgFilterType : nsISupports { + /* these longs are all actually of type nsMsgFilterTypeType */ + const long None = 0x00; + const long InboxRule = 0x01; + const long InboxJavaScript = 0x02; + const long Inbox = InboxRule | InboxJavaScript; + const long NewsRule = 0x04; + const long NewsJavaScript = 0x08; + const long News = NewsRule | NewsJavaScript; + const long Incoming = Inbox | News; + const long Manual = 0x10; + const long PostPlugin = 0x20; // After bayes filtering + const long PostOutgoing = 0x40; // After sending + const long Archive = 0x80; // Before archiving + const long Periodic = 0x100;// On a repeating timer + const long All = Incoming | Manual; +}; + +typedef long nsMsgFilterMotionValue; + +typedef long nsMsgFilterIndex; + +typedef long nsMsgRuleActionType; + +[scriptable, uuid(7726FE79-AFA3-4a39-8292-733AEE288737)] +interface nsMsgFilterAction : nsISupports { + + // Custom Action. + const long Custom=-1; + /* if you change these, you need to update filter.properties, + look for filterActionX */ + /* these longs are all actually of type nsMsgFilterActionType */ + const long None=0; /* uninitialized state */ + const long MoveToFolder=1; + const long ChangePriority=2; + const long Delete=3; + const long MarkRead=4; + const long KillThread=5; + const long WatchThread=6; + const long MarkFlagged=7; + const long Reply=9; + const long Forward=10; + const long StopExecution=11; + const long DeleteFromPop3Server=12; + const long LeaveOnPop3Server=13; + const long JunkScore=14; + const long FetchBodyFromPop3Server=15; + const long CopyToFolder=16; + const long AddTag=17; + const long KillSubthread=18; + const long MarkUnread=19; +}; diff --git a/comm/mailnews/search/public/nsMsgResultElement.h b/comm/mailnews/search/public/nsMsgResultElement.h new file mode 100644 index 0000000000..104e6a3771 --- /dev/null +++ b/comm/mailnews/search/public/nsMsgResultElement.h @@ -0,0 +1,40 @@ +/* -*- 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/. */ + +#ifndef __nsMsgResultElement_h +#define __nsMsgResultElement_h + +#include "nsMsgSearchCore.h" +#include "nsIMsgSearchAdapter.h" +#include "nsTArray.h" + +// nsMsgResultElement specifies a single search hit. + +//--------------------------------------------------------------------------- +// nsMsgResultElement is a list of attribute/value pairs which are used to +// represent a search hit without requiring a message header or server +// connection +//--------------------------------------------------------------------------- + +class nsMsgResultElement { + public: + explicit nsMsgResultElement(nsIMsgSearchAdapter*); + virtual ~nsMsgResultElement(); + + static nsresult AssignValues(nsIMsgSearchValue* src, nsMsgSearchValue* dst); + nsresult GetValue(nsMsgSearchAttribValue, nsMsgSearchValue**) const; + nsresult AddValue(nsIMsgSearchValue*); + nsresult AddValue(nsMsgSearchValue*); + + nsresult GetPrettyName(nsMsgSearchValue**); + nsresult Open(void* window); + + nsTArray<nsCOMPtr<nsIMsgSearchValue> > m_valueList; + nsIMsgSearchAdapter* m_adapter; + + protected: +}; + +#endif diff --git a/comm/mailnews/search/public/nsMsgSearchAdapter.h b/comm/mailnews/search/public/nsMsgSearchAdapter.h new file mode 100644 index 0000000000..fbfa5176e6 --- /dev/null +++ b/comm/mailnews/search/public/nsMsgSearchAdapter.h @@ -0,0 +1,242 @@ +/* -*- 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/. */ + +#ifndef _nsMsgSearchAdapter_H_ +#define _nsMsgSearchAdapter_H_ + +#include "nsMsgSearchCore.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsIMsgSearchAdapter.h" +#include "nsIMsgSearchValidityTable.h" +#include "nsIMsgSearchValidityManager.h" +#include "nsIMsgSearchTerm.h" +#include "nsINntpIncomingServer.h" + +class nsIMsgSearchScopeTerm; + +//----------------------------------------------------------------------------- +// These Adapter classes contain the smarts to convert search criteria from +// the canonical structures in msg_srch.h into whatever format is required +// by their protocol. +// +// There is a separate Adapter class for area (pop, imap, nntp, ldap) to contain +// the special smarts for that protocol. +//----------------------------------------------------------------------------- + +class nsMsgSearchAdapter : public nsIMsgSearchAdapter { + public: + nsMsgSearchAdapter(nsIMsgSearchScopeTerm*, + nsTArray<RefPtr<nsIMsgSearchTerm>> const&); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGSEARCHADAPTER + + nsIMsgSearchScopeTerm* m_scope; + nsTArray<RefPtr<nsIMsgSearchTerm>> + m_searchTerms; /* linked list of criteria terms */ + + nsString m_defaultCharset = u"UTF-8"_ns; + + static nsresult EncodeImap( + char** ppEncoding, nsTArray<RefPtr<nsIMsgSearchTerm>> const& searchTerms, + const char16_t* srcCharset, const char16_t* destCharset, + bool reallyDredd = false); + + static nsresult EncodeImapValue(char* encoding, const char* value, + bool useQuotes, bool reallyDredd); + + static char* GetImapCharsetParam(const char16_t* destCharset); + static char16_t* EscapeSearchUrl(const char16_t* nntpCommand); + static char16_t* EscapeImapSearchProtocol(const char16_t* imapCommand); + static char16_t* EscapeQuoteImapSearchProtocol(const char16_t* imapCommand); + static char* UnEscapeSearchUrl(const char* commandSpecificData); + // This stuff lives in the base class because the IMAP search syntax + // is used by the Dredd SEARCH command as well as IMAP itself + static const char* m_kImapBefore; + static const char* m_kImapBody; + static const char* m_kImapCC; + static const char* m_kImapFrom; + static const char* m_kImapNot; + static const char* m_kImapOr; + static const char* m_kImapSince; + static const char* m_kImapSubject; + static const char* m_kImapTo; + static const char* m_kImapHeader; + static const char* m_kImapAnyText; + static const char* m_kImapKeyword; + static const char* m_kNntpKeywords; + static const char* m_kImapSentOn; + static const char* m_kImapSeen; + static const char* m_kImapAnswered; + static const char* m_kImapNotSeen; + static const char* m_kImapNotAnswered; + static const char* m_kImapCharset; + static const char* m_kImapUnDeleted; + static const char* m_kImapSizeSmaller; + static const char* m_kImapSizeLarger; + static const char* m_kImapNew; + static const char* m_kImapNotNew; + static const char* m_kImapFlagged; + static const char* m_kImapNotFlagged; + + protected: + virtual ~nsMsgSearchAdapter(); + typedef enum _msg_TransformType { + kOverwrite, /* "John Doe" -> "John*Doe", simple contains */ + kInsert, /* "John Doe" -> "John* Doe", name completion */ + kSurround /* "John Doe" -> "John* *Doe", advanced contains */ + } msg_TransformType; + + char* TransformSpacesToStars(const char*, msg_TransformType transformType); + nsresult OpenNewsResultInUnknownGroup(nsMsgResultElement*); + + static nsresult EncodeImapTerm(nsIMsgSearchTerm*, bool reallyDredd, + const char16_t* srcCharset, + const char16_t* destCharset, char** ppOutTerm); +}; + +//----------------------------------------------------------------------------- +// Validity checking for attrib/op pairs. We need to know what operations are +// legal in three places: +// 1. when the FE brings up the dialog box and needs to know how to build +// the menus and enable their items +// 2. when the FE fires off a search, we need to check their lists for +// correctness +// 3. for on-the-fly capability negotiation e.g. with XSEARCH-capable news +// servers +//----------------------------------------------------------------------------- + +class nsMsgSearchValidityTable final : public nsIMsgSearchValidityTable { + public: + nsMsgSearchValidityTable(); + NS_DECL_NSIMSGSEARCHVALIDITYTABLE + NS_DECL_ISUPPORTS + + protected: + int m_numAvailAttribs; // number of rows with at least one available operator + typedef struct vtBits { + uint16_t bitEnabled : 1; + uint16_t bitAvailable : 1; + uint16_t bitValidButNotShown : 1; + } vtBits; + vtBits m_table[nsMsgSearchAttrib::kNumMsgSearchAttributes] + [nsMsgSearchOp::kNumMsgSearchOperators]; + + private: + ~nsMsgSearchValidityTable() {} + nsMsgSearchAttribValue m_defaultAttrib; +}; + +// Using getters and setters seems a little nicer then dumping the 2-D array +// syntax all over the code +#define CHECK_AO \ + if (a < 0 || a >= nsMsgSearchAttrib::kNumMsgSearchAttributes || o < 0 || \ + o >= nsMsgSearchOp::kNumMsgSearchOperators) \ + return NS_ERROR_ILLEGAL_VALUE; +inline nsresult nsMsgSearchValidityTable::SetAvailable(int a, int o, bool b) { + CHECK_AO; + m_table[a][o].bitAvailable = b; + return NS_OK; +} +inline nsresult nsMsgSearchValidityTable::SetEnabled(int a, int o, bool b) { + CHECK_AO; + m_table[a][o].bitEnabled = b; + return NS_OK; +} +inline nsresult nsMsgSearchValidityTable::SetValidButNotShown(int a, int o, + bool b) { + CHECK_AO; + m_table[a][o].bitValidButNotShown = b; + return NS_OK; +} + +inline nsresult nsMsgSearchValidityTable::GetAvailable(int a, int o, + bool* aResult) { + CHECK_AO; + *aResult = m_table[a][o].bitAvailable; + return NS_OK; +} +inline nsresult nsMsgSearchValidityTable::GetEnabled(int a, int o, + bool* aResult) { + CHECK_AO; + *aResult = m_table[a][o].bitEnabled; + return NS_OK; +} +inline nsresult nsMsgSearchValidityTable::GetValidButNotShown(int a, int o, + bool* aResult) { + CHECK_AO; + *aResult = m_table[a][o].bitValidButNotShown; + return NS_OK; +} +#undef CHECK_AO + +class nsMsgSearchValidityManager : public nsIMsgSearchValidityManager { + public: + nsMsgSearchValidityManager(); + + protected: + virtual ~nsMsgSearchValidityManager(); + + public: + NS_DECL_NSIMSGSEARCHVALIDITYMANAGER + NS_DECL_ISUPPORTS + + nsresult GetTable(int, nsMsgSearchValidityTable**); + + protected: + // There's one global validity manager that everyone uses. You *could* do + // this with static members of the adapter classes, but having a dedicated + // object makes cleanup of these tables (at shutdown-time) automagic. + + nsCOMPtr<nsIMsgSearchValidityTable> m_offlineMailTable; + nsCOMPtr<nsIMsgSearchValidityTable> m_offlineMailFilterTable; + nsCOMPtr<nsIMsgSearchValidityTable> m_onlineMailTable; + nsCOMPtr<nsIMsgSearchValidityTable> m_onlineMailFilterTable; + nsCOMPtr<nsIMsgSearchValidityTable> m_onlineManualFilterTable; + + nsCOMPtr<nsIMsgSearchValidityTable> m_newsTable; // online news + + // Local news tables, used for local news searching or offline. + nsCOMPtr<nsIMsgSearchValidityTable> m_localNewsTable; // base table + nsCOMPtr<nsIMsgSearchValidityTable> m_localNewsJunkTable; // base + junk + nsCOMPtr<nsIMsgSearchValidityTable> m_localNewsBodyTable; // base + body + nsCOMPtr<nsIMsgSearchValidityTable> + m_localNewsJunkBodyTable; // base + junk + body + nsCOMPtr<nsIMsgSearchValidityTable> m_ldapTable; + nsCOMPtr<nsIMsgSearchValidityTable> m_ldapAndTable; + nsCOMPtr<nsIMsgSearchValidityTable> m_localABTable; + nsCOMPtr<nsIMsgSearchValidityTable> m_localABAndTable; + nsCOMPtr<nsIMsgSearchValidityTable> m_newsFilterTable; + + nsresult NewTable(nsIMsgSearchValidityTable**); + + nsresult InitOfflineMailTable(); + nsresult InitOfflineMailFilterTable(); + nsresult InitOnlineMailTable(); + nsresult InitOnlineMailFilterTable(); + nsresult InitOnlineManualFilterTable(); + nsresult InitNewsTable(); + nsresult InitLocalNewsTable(); + nsresult InitLocalNewsJunkTable(); + nsresult InitLocalNewsBodyTable(); + nsresult InitLocalNewsJunkBodyTable(); + nsresult InitNewsFilterTable(); + + // set the custom headers in the table, changes whenever + // "mailnews.customHeaders" pref changes. + nsresult SetOtherHeadersInTable(nsIMsgSearchValidityTable* table, + const char* customHeaders); + + nsresult InitLdapTable(); + nsresult InitLdapAndTable(); + nsresult InitLocalABTable(); + nsresult InitLocalABAndTable(); + nsresult SetUpABTable(nsIMsgSearchValidityTable* aTable, bool isOrTable); + nsresult EnableDirectoryAttribute(nsIMsgSearchValidityTable* table, + nsMsgSearchAttribValue aSearchAttrib); +}; + +#endif diff --git a/comm/mailnews/search/public/nsMsgSearchBoolExpression.h b/comm/mailnews/search/public/nsMsgSearchBoolExpression.h new file mode 100644 index 0000000000..1eefd81fcc --- /dev/null +++ b/comm/mailnews/search/public/nsMsgSearchBoolExpression.h @@ -0,0 +1,110 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgSearchCore.h" + +#ifndef __nsMsgSearchBoolExpression_h +# define __nsMsgSearchBoolExpression_h + +//----------------------------------------------------------------------------- +// nsMsgSearchBoolExpression is a class added to provide AND/OR terms in search +// queries. +// A nsMsgSearchBoolExpression contains either a search term or two +// nsMsgSearchBoolExpressions and +// a boolean operator. +// I (mscott) am placing it here for now.... +//----------------------------------------------------------------------------- + +/* CBoolExpression --> encapsulates one or more search terms by internally + representing the search terms and their boolean operators as a binary + expression tree. Each node in the tree consists of either + (1) a boolean operator and two nsMsgSearchBoolExpressions or + (2) if the node is a leaf node then it contains a search term. + With each search term that is part of the expression we may also keep + a character string. The character + string is used to store the IMAP/NNTP encoding of the search term. This + makes generating a search encoding (for online) easier. + + For IMAP/NNTP: nsMsgSearchBoolExpression has/assumes knowledge about how + AND and OR search terms are combined according to IMAP4 and NNTP protocol. + That is the only piece of IMAP/NNTP knowledge it is aware of. + + Order of Evaluation: Okay, the way in which the boolean expression tree + is put together directly effects the order of evaluation. We currently + support left to right evaluation. + Supporting other order of evaluations involves adding new internal add + term methods. + */ + +class nsMsgSearchBoolExpression { + public: + // create a leaf node expression + explicit nsMsgSearchBoolExpression(nsIMsgSearchTerm* aNewTerm, + char* aEncodingString = NULL); + + // create a non-leaf node expression containing 2 expressions + // and a boolean operator + nsMsgSearchBoolExpression(nsMsgSearchBoolExpression*, + nsMsgSearchBoolExpression*, + nsMsgSearchBooleanOperator boolOp); + + nsMsgSearchBoolExpression(); + ~nsMsgSearchBoolExpression(); // recursively destroys all sub + // expressions as well + + // accessors + + // Offline + static nsMsgSearchBoolExpression* AddSearchTerm( + nsMsgSearchBoolExpression* aOrigExpr, nsIMsgSearchTerm* aNewTerm, + char* aEncodingStr); // IMAP/NNTP + static nsMsgSearchBoolExpression* AddExpressionTree( + nsMsgSearchBoolExpression* aOrigExpr, + nsMsgSearchBoolExpression* aExpression, bool aBoolOp); + + // parses the expression tree and all + // expressions underneath this node to + // determine if the end result is true or false. + bool OfflineEvaluate(nsIMsgDBHdr* msgToMatch, const char* defaultCharset, + nsIMsgSearchScopeTerm* scope, nsIMsgDatabase* db, + const nsACString& headers, bool Filtering); + + // assuming the expression is for online + // searches, determine the length of the + // resulting IMAP/NNTP encoding string + int32_t CalcEncodeStrSize(); + + // fills pre-allocated + // memory in buffer with + // the IMAP/NNTP encoding for the expression + void GenerateEncodeStr(nsCString* buffer); + + // if we are not a leaf node, then we have two other expressions + // and a boolean operator + nsMsgSearchBoolExpression* m_leftChild; + nsMsgSearchBoolExpression* m_rightChild; + nsMsgSearchBooleanOperator m_boolOp; + + protected: + // if we are a leaf node, all we have is a search term + + nsIMsgSearchTerm* m_term; + + // store IMAP/NNTP encoding for the search term if applicable + nsCString m_encodingStr; + + // internal methods + + // the idea is to separate the public interface for adding terms to + // the expression tree from the order of evaluation which influences + // how we internally construct the tree. Right now, we are supporting + // left to right evaluation so the tree is constructed to represent + // that by calling leftToRightAddTerm. If future forms of evaluation + // need to be supported, add new methods here for proper tree construction. + nsMsgSearchBoolExpression* leftToRightAddTerm(nsIMsgSearchTerm* newTerm, + char* encodingStr); +}; + +#endif diff --git a/comm/mailnews/search/public/nsMsgSearchCore.idl b/comm/mailnews/search/public/nsMsgSearchCore.idl new file mode 100644 index 0000000000..77b18dfa57 --- /dev/null +++ b/comm/mailnews/search/public/nsMsgSearchCore.idl @@ -0,0 +1,206 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "MailNewsTypes2.idl" + +interface nsIMsgFolder; + +interface nsIMsgDatabase; +interface nsIMsgDBHdr; + +[scriptable, uuid(6e893e59-af98-4f62-a326-0f00f32147cd)] + +interface nsMsgSearchScope : nsISupports { + const nsMsgSearchScopeValue offlineMail = 0; + const nsMsgSearchScopeValue offlineMailFilter = 1; + const nsMsgSearchScopeValue onlineMail = 2; + const nsMsgSearchScopeValue onlineMailFilter = 3; + /// offline news, base table, no body or junk + const nsMsgSearchScopeValue localNews = 4; + const nsMsgSearchScopeValue news = 5; + const nsMsgSearchScopeValue newsEx = 6; + const nsMsgSearchScopeValue LDAP = 7; + const nsMsgSearchScopeValue LocalAB = 8; + const nsMsgSearchScopeValue allSearchableGroups = 9; + const nsMsgSearchScopeValue newsFilter = 10; + const nsMsgSearchScopeValue LocalABAnd = 11; + const nsMsgSearchScopeValue LDAPAnd = 12; + // IMAP and NEWS, searched using local headers + const nsMsgSearchScopeValue onlineManual = 13; + /// local news + junk + const nsMsgSearchScopeValue localNewsJunk = 14; + /// local news + body + const nsMsgSearchScopeValue localNewsBody = 15; + /// local news + junk + body + const nsMsgSearchScopeValue localNewsJunkBody = 16; +}; + +typedef long nsMsgSearchAttribValue; + +/** + * Definitions of search attribute types. The numerical order + * from here will also be used to determine the order that the + * attributes display in the filter editor. + */ +[scriptable, uuid(a83ca7e8-4591-4111-8fb8-fd76ac73c866)] +interface nsMsgSearchAttrib : nsISupports { + const nsMsgSearchAttribValue Custom = -2; /* a custom term, see nsIMsgSearchCustomTerm */ + const nsMsgSearchAttribValue Default = -1; + const nsMsgSearchAttribValue Subject = 0; /* mail and news */ + const nsMsgSearchAttribValue Sender = 1; + const nsMsgSearchAttribValue Body = 2; + const nsMsgSearchAttribValue Date = 3; + + const nsMsgSearchAttribValue Priority = 4; /* mail only */ + const nsMsgSearchAttribValue MsgStatus = 5; + const nsMsgSearchAttribValue To = 6; + const nsMsgSearchAttribValue CC = 7; + const nsMsgSearchAttribValue ToOrCC = 8; + const nsMsgSearchAttribValue AllAddresses = 9; + + const nsMsgSearchAttribValue Location = 10; /* result list only */ + const nsMsgSearchAttribValue MessageKey = 11; /* message result elems */ + const nsMsgSearchAttribValue AgeInDays = 12; + const nsMsgSearchAttribValue FolderInfo = 13; /* for "view thread context" from result */ + const nsMsgSearchAttribValue Size = 14; + const nsMsgSearchAttribValue AnyText = 15; + const nsMsgSearchAttribValue Keywords = 16; // keywords are the internal representation of tags. + + const nsMsgSearchAttribValue Name = 17; + const nsMsgSearchAttribValue DisplayName = 18; + const nsMsgSearchAttribValue Nickname = 19; + const nsMsgSearchAttribValue ScreenName = 20; + const nsMsgSearchAttribValue Email = 21; + const nsMsgSearchAttribValue AdditionalEmail = 22; + const nsMsgSearchAttribValue PhoneNumber = 23; + const nsMsgSearchAttribValue WorkPhone = 24; + const nsMsgSearchAttribValue HomePhone = 25; + const nsMsgSearchAttribValue Fax = 26; + const nsMsgSearchAttribValue Pager = 27; + const nsMsgSearchAttribValue Mobile = 28; + const nsMsgSearchAttribValue City = 29; + const nsMsgSearchAttribValue Street = 30; + const nsMsgSearchAttribValue Title = 31; + const nsMsgSearchAttribValue Organization = 32; + const nsMsgSearchAttribValue Department = 33; + + // 34 - 43, reserved for ab / LDAP; + const nsMsgSearchAttribValue HasAttachmentStatus = 44; + const nsMsgSearchAttribValue JunkStatus = 45; + const nsMsgSearchAttribValue JunkPercent = 46; + const nsMsgSearchAttribValue JunkScoreOrigin = 47; + const nsMsgSearchAttribValue HdrProperty = 49; // uses nsIMsgSearchTerm::hdrProperty + const nsMsgSearchAttribValue FolderFlag = 50; // uses nsIMsgSearchTerm::status + const nsMsgSearchAttribValue Uint32HdrProperty = 51; // uses nsIMsgSearchTerm::hdrProperty + + // 52 is for showing customize - in ui headers start from 53 onwards up until 99. + + /** OtherHeader MUST ALWAYS BE LAST attribute since + * we can have an arbitrary # of these. The number can be changed, + * however, because we never persist AttribValues as integers. + */ + const nsMsgSearchAttribValue OtherHeader = 52; + // must be last attribute + const nsMsgSearchAttribValue kNumMsgSearchAttributes = 100; +}; + +typedef long nsMsgSearchOpValue; + +[scriptable, uuid(9160b196-6fcb-4eba-aaaf-6c806c4ee420)] +interface nsMsgSearchOp : nsISupports { + const nsMsgSearchOpValue Contains = 0; /* for text attributes */ + const nsMsgSearchOpValue DoesntContain = 1; + const nsMsgSearchOpValue Is = 2; /* is and isn't also apply to some non-text attrs */ + const nsMsgSearchOpValue Isnt = 3; + const nsMsgSearchOpValue IsEmpty = 4; + + const nsMsgSearchOpValue IsBefore = 5; /* for date attributes */ + const nsMsgSearchOpValue IsAfter = 6; + + const nsMsgSearchOpValue IsHigherThan = 7; /* for priority. Is also applies */ + const nsMsgSearchOpValue IsLowerThan = 8; + + const nsMsgSearchOpValue BeginsWith = 9; + const nsMsgSearchOpValue EndsWith = 10; + + const nsMsgSearchOpValue SoundsLike = 11; /* for LDAP phoenetic matching */ + const nsMsgSearchOpValue LdapDwim = 12; /* Do What I Mean for simple search */ + + const nsMsgSearchOpValue IsGreaterThan = 13; + const nsMsgSearchOpValue IsLessThan = 14; + + const nsMsgSearchOpValue NameCompletion = 15; /* Name Completion operator...as the name implies =) */ + const nsMsgSearchOpValue IsInAB = 16; + const nsMsgSearchOpValue IsntInAB = 17; + const nsMsgSearchOpValue IsntEmpty = 18; /* primarily for tags */ + const nsMsgSearchOpValue Matches = 19; /* generic term for use by custom terms */ + const nsMsgSearchOpValue DoesntMatch = 20; /* generic term for use by custom terms */ + const nsMsgSearchOpValue kNumMsgSearchOperators = 21; /* must be last operator */ +}; + +typedef long nsMsgSearchWidgetValue; + +/* FEs use this to help build the search dialog box */ +[scriptable,uuid(903dd2e8-304e-11d3-92e6-00a0c900d445)] +interface nsMsgSearchWidget : nsISupports { + const nsMsgSearchWidgetValue Text = 0; + const nsMsgSearchWidgetValue Date = 1; + const nsMsgSearchWidgetValue Menu = 2; + const nsMsgSearchWidgetValue Int = 3; /* added to account for age in days which requires an integer field */ + const nsMsgSearchWidgetValue None = 4; +}; + +typedef long nsMsgSearchBooleanOperator; + +[scriptable, uuid(a37f3f4a-304e-11d3-8f94-00a0c900d445)] +interface nsMsgSearchBooleanOp : nsISupports { + const nsMsgSearchBooleanOperator BooleanOR = 0; + const nsMsgSearchBooleanOperator BooleanAND = 1; +}; + +/* Use this to specify the value of a search term */ + +[ptr] native nsMsgSearchValue(nsMsgSearchValue); + +%{C++ +#include "nsString.h" +typedef struct nsMsgSearchValue +{ + nsMsgSearchAttribValue attribute; + union + { + nsMsgPriorityValue priority; + PRTime date; + uint32_t msgStatus; /* see MSG_FLAG in msgcom.h */ + uint32_t size; + nsMsgKey key; + int32_t age; /* in days */ + nsIMsgFolder *folder; + uint32_t junkStatus; + uint32_t junkPercent; + } u; + // We keep two versions of the string to avoid conversion at "search time". + nsCString utf8String; + nsString utf16String; +} nsMsgSearchValue; +%} + +[ptr] native nsMsgSearchTerm(nsMsgSearchTerm); + +// Please note the ! at the start of this macro, which means the macro +// needs to enumerate the non-string attributes. +%{C++ +#define IS_STRING_ATTRIBUTE(_a) \ +(!(_a == nsMsgSearchAttrib::Priority || _a == nsMsgSearchAttrib::Date || \ + _a == nsMsgSearchAttrib::MsgStatus || _a == nsMsgSearchAttrib::MessageKey || \ + _a == nsMsgSearchAttrib::Size || _a == nsMsgSearchAttrib::AgeInDays || \ + _a == nsMsgSearchAttrib::FolderInfo || _a == nsMsgSearchAttrib::Location || \ + _a == nsMsgSearchAttrib::JunkStatus || \ + _a == nsMsgSearchAttrib::FolderFlag || _a == nsMsgSearchAttrib::Uint32HdrProperty || \ + _a == nsMsgSearchAttrib::JunkPercent || _a == nsMsgSearchAttrib::HasAttachmentStatus)) +%} + +[ptr] native nsSearchMenuItem(nsSearchMenuItem); diff --git a/comm/mailnews/search/public/nsMsgSearchScopeTerm.h b/comm/mailnews/search/public/nsMsgSearchScopeTerm.h new file mode 100644 index 0000000000..b76cc676dc --- /dev/null +++ b/comm/mailnews/search/public/nsMsgSearchScopeTerm.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsMsgSearchScopeTerm_h +#define __nsMsgSearchScopeTerm_h + +#include "nsMsgSearchCore.h" +#include "nsMsgSearchScopeTerm.h" +#include "nsIMsgSearchAdapter.h" +#include "nsIMsgFolder.h" +#include "nsIMsgSearchAdapter.h" +#include "nsIMsgSearchSession.h" +#include "nsCOMPtr.h" +#include "nsIWeakReferenceUtils.h" + +class nsMsgSearchScopeTerm : public nsIMsgSearchScopeTerm { + public: + nsMsgSearchScopeTerm(nsIMsgSearchSession*, nsMsgSearchScopeValue, + nsIMsgFolder*); + nsMsgSearchScopeTerm(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGSEARCHSCOPETERM + + nsresult TimeSlice(bool* aDone); + nsresult InitializeAdapter( + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList); + + char* GetStatusBarName(); + + nsMsgSearchScopeValue m_attribute; + nsCOMPtr<nsIMsgFolder> m_folder; + nsCOMPtr<nsIMsgSearchAdapter> m_adapter; + nsCOMPtr<nsIInputStream> m_inputStream; // for message bodies + nsWeakPtr m_searchSession; + bool m_searchServer; + + private: + virtual ~nsMsgSearchScopeTerm(); +}; + +#endif diff --git a/comm/mailnews/search/public/nsMsgSearchTerm.h b/comm/mailnews/search/public/nsMsgSearchTerm.h new file mode 100644 index 0000000000..2ab5618cad --- /dev/null +++ b/comm/mailnews/search/public/nsMsgSearchTerm.h @@ -0,0 +1,89 @@ +/* -*- 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/. */ + +#ifndef __nsMsgSearchTerm_h +#define __nsMsgSearchTerm_h +//--------------------------------------------------------------------------- +// nsMsgSearchTerm specifies one criterion, e.g. name contains phil +//--------------------------------------------------------------------------- +#include "nsIMsgSearchSession.h" +#include "nsIMsgSearchScopeTerm.h" +#include "nsIMsgSearchTerm.h" +#include "nsIMsgSearchCustomTerm.h" + +// needed to search for addresses in address books +#include "nsIAbDirectory.h" + +#define EMPTY_MESSAGE_LINE(buf) \ + (buf[0] == '\r' || buf[0] == '\n' || buf[0] == '\0') + +class nsMsgSearchTerm : public nsIMsgSearchTerm { + public: + nsMsgSearchTerm(); + nsMsgSearchTerm(nsMsgSearchAttribValue, nsMsgSearchOpValue, + nsIMsgSearchValue*, nsMsgSearchBooleanOperator, + const char* arbitraryHeader); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGSEARCHTERM + + nsresult DeStream(char*, int16_t length); + nsresult DeStreamNew(char*, int16_t length); + + nsresult GetLocalTimes(PRTime, PRTime, PRExplodedTime&, PRExplodedTime&); + + bool IsBooleanOpAND() { + return m_booleanOp == nsMsgSearchBooleanOp::BooleanAND ? true : false; + } + nsMsgSearchBooleanOperator GetBooleanOp() { return m_booleanOp; } + // maybe should return nsString & ?? + const char* GetArbitraryHeader() { return m_arbitraryHeader.get(); } + + static char* EscapeQuotesInStr(const char* str); + + nsMsgSearchAttribValue m_attribute; + nsMsgSearchOpValue m_operator; + nsMsgSearchValue m_value; + + // boolean operator to be applied to this search term and the search term + // which precedes it. + nsMsgSearchBooleanOperator m_booleanOp; + + // user specified string for the name of the arbitrary header to be used in + // the search only has a value when m_attribute = OtherHeader!!!! + nsCString m_arbitraryHeader; + + // db hdr property name to use - used when m_attribute = HdrProperty. + nsCString m_hdrProperty; + bool m_matchAll; // does this term match all headers? + nsCString m_customId; // id of custom search term + + protected: + virtual ~nsMsgSearchTerm(); + + nsresult MatchString(const nsACString& stringToMatch, const char* charset, + bool* pResult); + nsresult MatchString(const nsAString& stringToMatch, bool* pResult); + nsresult OutputValue(nsCString& outputStr); + nsresult ParseAttribute(char* inStream, nsMsgSearchAttribValue* attrib); + nsresult ParseOperator(char* inStream, nsMsgSearchOpValue* value); + nsresult ParseValue(char* inStream); + /** + * Switch a string to lower case, except for special database rows + * that are not headers, but could be headers + * + * @param aValue the string to switch + */ + void ToLowerCaseExceptSpecials(nsACString& aValue); + nsresult InitializeAddressBook(); + nsresult MatchInAddressBook(const nsAString& aAddress, bool* pResult); + // fields used by search in address book + nsCOMPtr<nsIAbDirectory> mDirectory; + + bool mBeginsGrouping; + bool mEndsGrouping; +}; + +#endif diff --git a/comm/mailnews/search/src/Bogofilter.sfd b/comm/mailnews/search/src/Bogofilter.sfd new file mode 100644 index 0000000000..a29a818e69 --- /dev/null +++ b/comm/mailnews/search/src/Bogofilter.sfd @@ -0,0 +1,14 @@ +version="9" +logging="yes" +name="BogofilterYes" +enabled="yes" +type="17" +action="JunkScore" +actionValue="100" +condition="OR (\"X-Bogosity\",begins with,Spam) OR (\"X-Bogosity\",begins with,Y)" +name="BogofilterNo" +enabled="yes" +type="17" +action="JunkScore" +actionValue="0" +condition="OR (\"X-Bogosity\",begins with,Ham) OR (\"X-Bogosity\",begins with,N)" diff --git a/comm/mailnews/search/src/DSPAM.sfd b/comm/mailnews/search/src/DSPAM.sfd new file mode 100644 index 0000000000..40bc00df78 --- /dev/null +++ b/comm/mailnews/search/src/DSPAM.sfd @@ -0,0 +1,14 @@ +version="9" +logging="yes" +name="DSPAMYes" +enabled="yes" +type="17" +action="JunkScore" +actionValue="100" +condition="OR (\"X-DSPAM-Result\",begins with,Spam)" +name="DSPAMNo" +enabled="yes" +type="17" +action="JunkScore" +actionValue="0" +condition="OR (\"X-DSPAM-Result\",begins with,Innocent)" diff --git a/comm/mailnews/search/src/Habeas.sfd b/comm/mailnews/search/src/Habeas.sfd new file mode 100644 index 0000000000..ceffff16e5 --- /dev/null +++ b/comm/mailnews/search/src/Habeas.sfd @@ -0,0 +1,8 @@ +version="8" +logging="yes" +name="HabeasNo" +enabled="yes" +type="1" +action="JunkScore" +actionValue="0" +condition="OR (\"X-Habeas-SWE-3\",is,\"like Habeas SWE (tm)\")" diff --git a/comm/mailnews/search/src/MsgTraitService.jsm b/comm/mailnews/search/src/MsgTraitService.jsm new file mode 100644 index 0000000000..c44b015e38 --- /dev/null +++ b/comm/mailnews/search/src/MsgTraitService.jsm @@ -0,0 +1,199 @@ +/* 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 EXPORTED_SYMBOLS = ["MsgTraitService"]; + +// local static variables + +var _lastIndex = 0; // the first index will be one +var _traits = {}; + +var traitsBranch = Services.prefs.getBranch("mailnews.traits."); + +function _registerTrait(aId, aIndex) { + var trait = {}; + trait.enabled = false; + trait.name = ""; + trait.antiId = ""; + trait.index = aIndex; + _traits[aId] = trait; +} + +function MsgTraitService() {} + +MsgTraitService.prototype = { + // Component setup + QueryInterface: ChromeUtils.generateQI(["nsIMsgTraitService"]), + + // nsIMsgTraitService implementation + + get lastIndex() { + return _lastIndex; + }, + + registerTrait(aId) { + if (_traits[aId]) { + // Meaning already registered. + return 0; + } + _registerTrait(aId, ++_lastIndex); + traitsBranch.setBoolPref("enabled." + _lastIndex, false); + traitsBranch.setCharPref("id." + _lastIndex, aId); + return _lastIndex; + }, + + unRegisterTrait(aId) { + if (_traits[aId]) { + var index = _traits[aId].index; + _traits[aId] = null; + traitsBranch.clearUserPref("id." + index); + traitsBranch.clearUserPref("enabled." + index); + traitsBranch.clearUserPref("antiId." + index); + traitsBranch.clearUserPref("name." + index); + } + }, + + isRegistered(aId) { + return !!_traits[aId]; + }, + + setName(aId, aName) { + traitsBranch.setCharPref("name." + _traits[aId].index, aName); + _traits[aId].name = aName; + }, + + getName(aId) { + return _traits[aId].name; + }, + + getIndex(aId) { + return _traits[aId].index; + }, + + getId(aIndex) { + for (let id in _traits) { + if (_traits[id].index == aIndex) { + return id; + } + } + return null; + }, + + setEnabled(aId, aEnabled) { + traitsBranch.setBoolPref("enabled." + _traits[aId].index, aEnabled); + _traits[aId].enabled = aEnabled; + }, + + getEnabled(aId) { + return _traits[aId].enabled; + }, + + setAntiId(aId, aAntiId) { + traitsBranch.setCharPref("antiId." + _traits[aId].index, aAntiId); + _traits[aId].antiId = aAntiId; + }, + + getAntiId(aId) { + return _traits[aId].antiId; + }, + + getEnabledProIndices() { + let proIndices = []; + for (let id in _traits) { + if (_traits[id].enabled) { + proIndices.push(_traits[id].index); + } + } + return proIndices; + }, + + getEnabledAntiIndices() { + let antiIndices = []; + for (let id in _traits) { + if (_traits[id].enabled) { + antiIndices.push(_traits[_traits[id].antiId].index); + } + } + return antiIndices; + }, + + addAlias(aTraitIndex, aTraitAliasIndex) { + let aliasesString = traitsBranch.getCharPref("aliases." + aTraitIndex, ""); + let aliases; + if (aliasesString.length) { + aliases = aliasesString.split(","); + } else { + aliases = []; + } + if (!aliases.includes(aTraitAliasIndex.toString())) { + aliases.push(aTraitAliasIndex); + traitsBranch.setCharPref("aliases." + aTraitIndex, aliases.join()); + } + }, + + removeAlias(aTraitIndex, aTraitAliasIndex) { + let aliasesString = traitsBranch.getCharPref("aliases." + aTraitIndex, ""); + let aliases; + if (aliasesString.length) { + aliases = aliasesString.split(","); + } else { + aliases = []; + } + let location = aliases.indexOf(aTraitAliasIndex.toString()); + if (location != -1) { + aliases.splice(location, 1); + traitsBranch.setCharPref("aliases." + aTraitIndex, aliases.join()); + } + }, + + getAliases(aTraitIndex) { + let aliasesString = traitsBranch.getCharPref("aliases." + aTraitIndex, ""); + let aliases; + if (aliasesString.length) { + aliases = aliasesString.split(","); + } else { + aliases = []; + } + return aliases; + }, +}; + +// initialization + +_init(); + +function _init() { + // get existing traits + var idBranch = Services.prefs.getBranch("mailnews.traits.id."); + var nameBranch = Services.prefs.getBranch("mailnews.traits.name."); + var enabledBranch = Services.prefs.getBranch("mailnews.traits.enabled."); + var antiIdBranch = Services.prefs.getBranch("mailnews.traits.antiId."); + _lastIndex = Services.prefs + .getBranch("mailnews.traits.") + .getIntPref("lastIndex"); + var ids = idBranch.getChildList(""); + for (let i = 0; i < ids.length; i++) { + var id = idBranch.getCharPref(ids[i]); + var index = parseInt(ids[i]); + _registerTrait(id, index, false); + + if (nameBranch.getPrefType(ids[i]) == Services.prefs.PREF_STRING) { + _traits[id].name = nameBranch.getCharPref(ids[i]); + } + if (enabledBranch.getPrefType(ids[i]) == Services.prefs.PREF_BOOL) { + _traits[id].enabled = enabledBranch.getBoolPref(ids[i]); + } + if (antiIdBranch.getPrefType(ids[i]) == Services.prefs.PREF_STRING) { + _traits[id].antiId = antiIdBranch.getCharPref(ids[i]); + } + + if (_lastIndex < index) { + _lastIndex = index; + } + } + + // for (traitId in _traits) + // dump("\nindex of " + traitId + " is " + _traits[traitId].index); + // dump("\n"); +} diff --git a/comm/mailnews/search/src/POPFile.sfd b/comm/mailnews/search/src/POPFile.sfd new file mode 100644 index 0000000000..a791705aa4 --- /dev/null +++ b/comm/mailnews/search/src/POPFile.sfd @@ -0,0 +1,14 @@ +version="9" +logging="yes" +name="POPFileYes" +enabled="yes" +type="17" +action="JunkScore" +actionValue="100" +condition="OR (\"X-Text-Classification\",begins with,spam)" +name="POPFileNo" +enabled="yes" +type="17" +action="JunkScore" +actionValue="0" +condition="OR (\"X-Text-Classification\",begins with,inbox) OR (\"X-Text-Classification\",begins with,allowed)" diff --git a/comm/mailnews/search/src/PeriodicFilterManager.jsm b/comm/mailnews/search/src/PeriodicFilterManager.jsm new file mode 100644 index 0000000000..3d6f52c504 --- /dev/null +++ b/comm/mailnews/search/src/PeriodicFilterManager.jsm @@ -0,0 +1,202 @@ +/* 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/. */ + +/* + * Execute periodic filters at the correct rate. + * + * The only external call required for this is setupFiltering(). This should be + * called before the mail-startup-done notification. + */ + +const EXPORTED_SYMBOLS = ["PeriodicFilterManager"]; + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +const log = console.createInstance({ + prefix: "mail.periodicfilters", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.periodicfilters.loglevel", +}); + +var PeriodicFilterManager = { + _timer: null, + _checkRateMilliseconds: 60000, // How often do we check if servers are ready to run? + _defaultFilterRateMinutes: Services.prefs + .getDefaultBranch("") + .getIntPref("mail.server.default.periodicFilterRateMinutes"), + _initialized: false, // Has this been initialized? + _running: false, // Are we executing filters already? + + // Initial call to begin startup. + setupFiltering() { + if (this._initialized) { + return; + } + + this._initialized = true; + Services.obs.addObserver(this, "mail-startup-done"); + }, + + // Main call to start the periodic filter process + init() { + log.info("PeriodicFilterManager init()"); + // set the next filter time + for (let server of MailServices.accounts.allServers) { + let nowTime = parseInt(Date.now() / 60000); + // Make sure that the last filter time of all servers was in the past. + let lastFilterTime = server.getIntValue("lastFilterTime"); + // Schedule next filter run. + let nextFilterTime = + lastFilterTime < nowTime + ? lastFilterTime + this.getServerPeriod(server) + : nowTime; + server.setIntValue("nextFilterTime", nextFilterTime); + } + + // kickoff the timer to run periodic filters + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this._timer.initWithCallback( + this, + this._checkRateMilliseconds, + Ci.nsITimer.TYPE_REPEATING_SLACK + ); + Services.obs.addObserver(this, "quit-application-granted"); + }, + + /** + * Periodic callback to check if any periodic filters need to be run. + * + * The periodic filter manager does not guarantee that filters will be run + * precisely at the specified interval. + * The server may be busy (e.g. downloading messages) or another filter run + * is still ongoing, in which cases running periodic filter of any server + * may be postponed. + */ + notify(timer) { + log.debug("PeriodicFilterManager timer callback"); + if (this._running) { + log.debug("PeriodicFilterManager Previous filter run still executing"); + return; + } + this._running = true; + let nowTime = parseInt(Date.now() / 60000); + for (let server of MailServices.accounts.allServers) { + if (!server.canHaveFilters) { + continue; + } + if (server.getIntValue("nextFilterTime") > nowTime) { + continue; + } + if (server.serverBusy) { + continue; + } + + // Schedule next time this account's filters should be run. + server.setIntValue( + "nextFilterTime", + nowTime + this.getServerPeriod(server) + ); + server.setIntValue("lastFilterTime", nowTime); + + // Build a temporary list of periodic filters. + // XXX TODO: make applyFiltersToFolders() take a filterType instead (bug 1551043). + let curFilterList = server.getFilterList(null); + let tempFilterList = MailServices.filters.getTempFilterList( + server.rootFolder + ); + let numFilters = curFilterList.filterCount; + tempFilterList.loggingEnabled = curFilterList.loggingEnabled; + tempFilterList.logStream = curFilterList.logStream; + let newFilterIndex = 0; + for (let i = 0; i < numFilters; i++) { + let curFilter = curFilterList.getFilterAt(i); + // Only add enabled, UI visible filters that are of the Periodic type. + if ( + curFilter.enabled && + !curFilter.temporary && + curFilter.filterType & Ci.nsMsgFilterType.Periodic + ) { + tempFilterList.insertFilterAt(newFilterIndex, curFilter); + newFilterIndex++; + } + } + if (newFilterIndex == 0) { + continue; + } + let foldersToFilter = server.rootFolder.getFoldersWithFlags( + Ci.nsMsgFolderFlags.Inbox + ); + if (foldersToFilter.length == 0) { + continue; + } + + log.debug( + "PeriodicFilterManager apply periodic filters to server " + + server.prettyName + ); + MailServices.filters.applyFiltersToFolders( + tempFilterList, + foldersToFilter, + null + ); + } + this._running = false; + }, + + /** + * Gets the periodic filter interval for the given server. + * If the server's interval is not sane, clean it up. + * + * @param {nsIMsgIncomingServer} server - The server to return interval for. + */ + getServerPeriod(server) { + const minimumPeriodMinutes = 1; + let serverRateMinutes = server.getIntValue("periodicFilterRateMinutes"); + // Check if period is too short. + if (serverRateMinutes < minimumPeriodMinutes) { + // If the server.default pref is too low, clear that one first. + if ( + Services.prefs.getIntPref( + "mail.server.default.periodicFilterRateMinutes" + ) == serverRateMinutes + ) { + Services.prefs.clearUserPref( + "mail.server.default.periodicFilterRateMinutes" + ); + } + // If the server still has its own specific value and it is still too low, sanitize it. + if ( + server.getIntValue("periodicFilterRateMinutes") < minimumPeriodMinutes + ) { + server.setIntValue( + "periodicFilterRateMinutes", + this._defaultFilterRateMinutes + ); + } + + return this._defaultFilterRateMinutes; + } + + return serverRateMinutes; + }, + + observe(subject, topic, data) { + Services.obs.removeObserver(this, topic); + if (topic == "mail-startup-done") { + this.init(); + } else if (topic == "quit-application-granted") { + this.shutdown(); + } + }, + + shutdown() { + log.info("PeriodicFilterManager shutdown"); + if (this._timer) { + this._timer.cancel(); + this._timer = null; + } + }, +}; diff --git a/comm/mailnews/search/src/SpamAssassin.sfd b/comm/mailnews/search/src/SpamAssassin.sfd new file mode 100644 index 0000000000..d8d0ecdb10 --- /dev/null +++ b/comm/mailnews/search/src/SpamAssassin.sfd @@ -0,0 +1,14 @@ +version="9" +logging="yes" +name="SpamAssassinYes" +enabled="yes" +type="17" +action="JunkScore" +actionValue="100" +condition="OR (\"X-Spam-Status\",begins with,Yes) OR (\"X-Spam-Flag\",begins with,YES) OR (subject,begins with,***SPAM***)" +name="SpamAssassinNo" +enabled="yes" +type="17" +action="JunkScore" +actionValue="0" +condition="OR (\"X-Spam-Status\",begins with,No)" diff --git a/comm/mailnews/search/src/SpamCatcher.sfd b/comm/mailnews/search/src/SpamCatcher.sfd new file mode 100644 index 0000000000..d16cd80a28 --- /dev/null +++ b/comm/mailnews/search/src/SpamCatcher.sfd @@ -0,0 +1,14 @@ +version="9" +logging="yes" +name="SpamCatcherNo" +enabled="yes" +type="17" +action="JunkScore" +actionValue="0" +condition="OR (\"x-SpamCatcher\",begins with,No)" +name="SpamCatcherYes" +enabled="yes" +type="17" +action="JunkScore" +actionValue="100" +condition="OR (\"x-SpamCatcher\",begins with,Yes)" diff --git a/comm/mailnews/search/src/SpamPal.sfd b/comm/mailnews/search/src/SpamPal.sfd new file mode 100644 index 0000000000..830b1937b5 --- /dev/null +++ b/comm/mailnews/search/src/SpamPal.sfd @@ -0,0 +1,14 @@ +version="9" +logging="yes" +name="SpamPalNo" +enabled="yes" +type="17" +action="JunkScore" +actionValue="0" +condition="OR (\"X-SpamPal\",begins with,PASS)" +name="SpamPalYes" +enabled="yes" +type="17" +action="JunkScore" +actionValue="100" +condition="OR (\"X-SpamPal\",begins with,SPAM)" diff --git a/comm/mailnews/search/src/components.conf b/comm/mailnews/search/src/components.conf new file mode 100644 index 0000000000..943b3fb542 --- /dev/null +++ b/comm/mailnews/search/src/components.conf @@ -0,0 +1,40 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + "cid": "{a2e95f4f-da72-4a41-9493-661ad353c00a}", + "contract_ids": ["@mozilla.org/msg-trait-service;1"], + "jsm": "resource:///modules/MsgTraitService.jsm", + "constructor": "MsgTraitService", + }, + { + "cid": "{5cbb0700-04bc-11d3-a50a-0060b0fc04b7}", + "contract_ids": ["@mozilla.org/messenger/services/filters;1"], + "type": "nsMsgFilterService", + "headers": ["/comm/mailnews/search/src/nsMsgFilterService.h"], + "name": "Filter", + "interfaces": ["nsIMsgFilterService"], + }, + { + "cid": "{e9a7cd70-0303-11d3-a50a-0060b0fc04b7}", + "contract_ids": ["@mozilla.org/messenger/searchSession;1"], + "type": "nsMsgSearchSession", + "headers": ["/comm/mailnews/search/src/nsMsgSearchSession.h"], + }, + { + "cid": "{e1da397d-fdc5-4b23-a6fe-d46a034d80b3}", + "contract_ids": ["@mozilla.org/messenger/searchTerm;1"], + "type": "nsMsgSearchTerm", + "headers": ["/comm/mailnews/search/public/nsMsgSearchTerm.h"], + }, + { + "cid": "{1510faee-ad1a-4194-8039-33de32d5a882}", + "contract_ids": ["@mozilla.org/mail/search/validityManager;1"], + "type": "nsMsgSearchValidityManager", + "headers": ["/comm/mailnews/search/public/nsMsgSearchAdapter.h"], + }, +] diff --git a/comm/mailnews/search/src/moz.build b/comm/mailnews/search/src/moz.build new file mode 100644 index 0000000000..b8f879aa15 --- /dev/null +++ b/comm/mailnews/search/src/moz.build @@ -0,0 +1,37 @@ +# vim: set filetype=python: +# 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/. + +SOURCES += [ + "nsMsgBodyHandler.cpp", + "nsMsgFilter.cpp", + "nsMsgFilterList.cpp", + "nsMsgFilterService.cpp", + "nsMsgImapSearch.cpp", + "nsMsgLocalSearch.cpp", + "nsMsgSearchAdapter.cpp", + "nsMsgSearchNews.cpp", + "nsMsgSearchSession.cpp", + "nsMsgSearchTerm.cpp", + "nsMsgSearchValue.cpp", +] + +EXTRA_JS_MODULES += [ + "MsgTraitService.jsm", + "PeriodicFilterManager.jsm", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +FINAL_LIBRARY = "mail" + +FINAL_TARGET_FILES.isp += [ + "Bogofilter.sfd", + "DSPAM.sfd", + "POPFile.sfd", + "SpamAssassin.sfd", + "SpamPal.sfd", +] diff --git a/comm/mailnews/search/src/nsMsgBodyHandler.cpp b/comm/mailnews/search/src/nsMsgBodyHandler.cpp new file mode 100644 index 0000000000..5b77750f63 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgBodyHandler.cpp @@ -0,0 +1,464 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgSearchCore.h" +#include "nsMsgUtils.h" +#include "nsMsgBodyHandler.h" +#include "nsMsgSearchTerm.h" +#include "nsIMsgHdr.h" +#include "nsMsgMessageFlags.h" +#include "nsISeekableStream.h" +#include "nsIInputStream.h" +#include "nsIFile.h" +#include "plbase64.h" +#include "prmem.h" +#include "nsMimeTypes.h" + +nsMsgBodyHandler::nsMsgBodyHandler(nsIMsgSearchScopeTerm* scope, + uint32_t numLines, nsIMsgDBHdr* msg, + nsIMsgDatabase* db) { + m_scope = scope; + m_numLocalLines = numLines; + uint32_t flags; + m_lineCountInBodyLines = NS_SUCCEEDED(msg->GetFlags(&flags)) + ? !(flags & nsMsgMessageFlags::Offline) + : true; + // account for added x-mozilla-status lines, and envelope line. + if (!m_lineCountInBodyLines) m_numLocalLines += 3; + m_msgHdr = msg; + m_db = db; + + // the following are variables used when the body handler is handling stuff + // from filters....through this constructor, that is not the case so we set + // them to NULL. + m_headers = nullptr; + m_headersSize = 0; + m_Filtering = false; // make sure we set this before we call initialize... + + Initialize(); // common initialization stuff + OpenLocalFolder(); +} + +nsMsgBodyHandler::nsMsgBodyHandler(nsIMsgSearchScopeTerm* scope, + uint32_t numLines, nsIMsgDBHdr* msg, + nsIMsgDatabase* db, const char* headers, + uint32_t headersSize, bool Filtering) { + m_scope = scope; + m_numLocalLines = numLines; + uint32_t flags; + m_lineCountInBodyLines = NS_SUCCEEDED(msg->GetFlags(&flags)) + ? !(flags & nsMsgMessageFlags::Offline) + : true; + // account for added x-mozilla-status lines, and envelope line. + if (!m_lineCountInBodyLines) m_numLocalLines += 3; + m_msgHdr = msg; + m_db = db; + m_headers = nullptr; + m_headersSize = 0; + m_Filtering = Filtering; + + Initialize(); + + if (m_Filtering) { + m_headers = headers; + m_headersSize = headersSize; + } else { + OpenLocalFolder(); + } +} + +void nsMsgBodyHandler::Initialize() +// common initialization code regardless of what body type we are handling... +{ + // Default transformations for local message search and MAPI access + m_stripHeaders = true; + m_partIsHtml = false; + m_base64part = false; + m_partIsQP = false; + m_isMultipart = false; + m_partIsText = true; // Default is text/plain, maybe proven otherwise later. + m_pastMsgHeaders = false; + m_pastPartHeaders = false; + m_inMessageAttachment = false; + m_headerBytesRead = 0; +} + +nsMsgBodyHandler::~nsMsgBodyHandler() {} + +int32_t nsMsgBodyHandler::GetNextLine(nsCString& buf, nsCString& charset) { + int32_t length = -1; // length of incoming line or -1 eof + int32_t outLength = -1; // length of outgoing line or -1 eof + bool eatThisLine = true; + nsAutoCString nextLine; + + while (eatThisLine) { + // first, handle the filtering case...this is easy.... + if (m_Filtering) { + length = GetNextFilterLine(nextLine); + } else { + // 3 cases: Offline IMAP, POP, or we are dealing with a news message.... + // Offline cases should be same as local mail cases, since we're going + // to store offline messages in berkeley format folders. + if (m_db) { + length = GetNextLocalLine(nextLine); // (2) POP + } + } + + if (length < 0) break; // eof in + + outLength = ApplyTransformations(nextLine, length, eatThisLine, buf); + } + + if (outLength < 0) return -1; // eof out + + // For non-multipart messages, the entire message minus headers is encoded + // ApplyTransformations can only decode a part + if (!m_isMultipart && m_base64part) { + Base64Decode(buf); + m_base64part = false; + // And reapply our transformations... + outLength = ApplyTransformations(buf, buf.Length(), eatThisLine, buf); + } + + // Process aggregated HTML. + if (!m_isMultipart && m_partIsHtml) { + StripHtml(buf); + outLength = buf.Length(); + } + + charset = m_partCharset; + return outLength; +} + +void nsMsgBodyHandler::OpenLocalFolder() { + nsCOMPtr<nsIInputStream> inputStream; + nsresult rv = m_scope->GetInputStream(m_msgHdr, getter_AddRefs(inputStream)); + // Warn and return if GetInputStream fails + NS_ENSURE_SUCCESS_VOID(rv); + m_fileLineStream = do_QueryInterface(inputStream); +} + +int32_t nsMsgBodyHandler::GetNextFilterLine(nsCString& buf) { + // m_nextHdr always points to the next header in the list....the list is NULL + // terminated... + uint32_t numBytesCopied = 0; + if (m_headersSize > 0) { + // #mscott. Ugly hack! filter headers list have CRs & LFs inside the NULL + // delimited list of header strings. It is possible to have: To NULL CR LF + // From. We want to skip over these CR/LFs if they start at the beginning of + // what we think is another header. + + while (m_headersSize > 0 && (m_headers[0] == '\r' || m_headers[0] == '\n' || + m_headers[0] == ' ' || m_headers[0] == '\0')) { + m_headers++; // skip over these chars... + m_headersSize--; + } + + if (m_headersSize > 0) { + numBytesCopied = strlen(m_headers) + 1; + buf.Assign(m_headers); + m_headers += numBytesCopied; + // be careful...m_headersSize is unsigned. Don't let it go negative or we + // overflow to 2^32....*yikes* + if (m_headersSize < numBytesCopied) + m_headersSize = 0; + else + m_headersSize -= numBytesCopied; // update # bytes we have read from + // the headers list + + return (int32_t)numBytesCopied; + } + } else if (m_headersSize == 0) { + buf.Truncate(); + } + return -1; +} + +// return -1 if no more local lines, length of next line otherwise. + +int32_t nsMsgBodyHandler::GetNextLocalLine(nsCString& buf) +// returns number of bytes copied +{ + if (m_numLocalLines) { + // I the line count is in body lines, only decrement once we have + // processed all the headers. Otherwise the line is not in body + // lines and we want to decrement for every line. + if (m_pastMsgHeaders || !m_lineCountInBodyLines) m_numLocalLines--; + // do we need to check the return value here? + if (m_fileLineStream) { + bool more = false; + nsresult rv = m_fileLineStream->ReadLine(buf, &more); + if (NS_SUCCEEDED(rv)) return buf.Length(); + } + } + + return -1; +} + +/** + * This method applies a sequence of transformations to the line. + * + * It applies the following sequences in order + * * Removes headers if the searcher doesn't want them + * (sets m_past*Headers) + * * Determines the current MIME type. + * (via SniffPossibleMIMEHeader) + * * Strips any HTML if the searcher doesn't want it + * * Strips non-text parts + * * Decodes any base64 part + * (resetting part variables: m_base64part, m_pastPartHeaders, m_partIsHtml, + * m_partIsText) + * + * @param line (in) the current line + * @param length (in) the length of said line + * @param eatThisLine (out) whether or not to ignore this line + * @param buf (inout) if m_base64part, the current part as needed for + * decoding; else, it is treated as an out param (a + * redundant version of line). + * @return the length of the line after applying transformations + */ +int32_t nsMsgBodyHandler::ApplyTransformations(const nsCString& line, + int32_t length, + bool& eatThisLine, + nsCString& buf) { + eatThisLine = false; + + if (!m_pastPartHeaders) // line is a line from the part headers + { + if (m_stripHeaders) eatThisLine = true; + + // We have already grabbed all worthwhile information from the headers, + // so there is no need to keep track of the current lines + buf.Assign(line); + + SniffPossibleMIMEHeader(buf); + + if (buf.IsEmpty() || buf.First() == '\r' || buf.First() == '\n') { + if (!m_inMessageAttachment) { + m_pastPartHeaders = true; + } else { + // We're in a message attachment and have just read past the + // part header for the attached message. We now need to read + // the message headers and any part headers. + // We can now forget about the special handling of attached messages. + m_inMessageAttachment = false; + } + } + + // We set m_pastMsgHeaders to 'true' only once. + if (m_pastPartHeaders) m_pastMsgHeaders = true; + + return length; + } + + // Check to see if this is one of our boundary strings. + bool matchedBoundary = false; + if (m_isMultipart && m_boundaries.Length() > 0) { + for (int32_t i = (int32_t)m_boundaries.Length() - 1; i >= 0; i--) { + if (StringBeginsWith(line, m_boundaries[i])) { + matchedBoundary = true; + // If we matched a boundary, we won't need the nested/later ones any + // more. + m_boundaries.SetLength(i + 1); + break; + } + } + } + if (matchedBoundary) { + if (m_base64part && m_partIsText) { + Base64Decode(buf); + // Work on the parsed string + if (!buf.Length()) { + NS_WARNING("Trying to transform an empty buffer"); + eatThisLine = true; + } else { + // It is wrong to call ApplyTransformations() here since this will + // lead to the buffer being doubled-up at |buf.Append(line);| + // below. ApplyTransformations(buf, buf.Length(), eatThisLine, buf); + // Avoid spurious failures + eatThisLine = false; + } + } else if (!m_partIsHtml) { + buf.Truncate(); + eatThisLine = true; // We have no content... + } + + if (m_partIsHtml) { + StripHtml(buf); + } + + // Reset all assumed headers + m_base64part = false; + // Get ready to sniff new part headers, but do not reset m_pastMsgHeaders + // since it will screw the body line count. + m_pastPartHeaders = false; + m_partIsHtml = false; + // If we ever see a multipart message, each part needs to set + // 'm_partIsText', so no more defaulting to 'true' when the part is done. + m_partIsText = false; + + // Note: we cannot reset 'm_partIsQP' yet since we still need it to process + // the last buffer returned here. Parsing the next part will set a new + // value. + return buf.Length(); + } + + if (!m_partIsText) { + // Ignore non-text parts + buf.Truncate(); + eatThisLine = true; + return 0; + } + + // Accumulate base64 parts and HTML parts for later decoding or tag stripping. + if (m_base64part || m_partIsHtml) { + if (m_partIsHtml && !m_base64part) { + size_t bufLength = buf.Length(); + if (!m_partIsQP || bufLength == 0 || !StringEndsWith(buf, "="_ns)) { + // Replace newline in HTML with a space. + buf.Append(' '); + } else { + // Strip the soft line break. + buf.SetLength(bufLength - 1); + } + } + buf.Append(line); + eatThisLine = true; + return buf.Length(); + } + + buf.Assign(line); + return buf.Length(); +} + +void nsMsgBodyHandler::StripHtml(nsCString& pBufInOut) { + char* pBuf = (char*)PR_Malloc(pBufInOut.Length() + 1); + if (pBuf) { + char* pWalk = pBuf; + + char* pWalkInOut = (char*)pBufInOut.get(); + bool inTag = false; + while (*pWalkInOut) // throw away everything inside < > + { + if (!inTag) { + if (*pWalkInOut == '<') + inTag = true; + else + *pWalk++ = *pWalkInOut; + } else { + if (*pWalkInOut == '>') inTag = false; + } + pWalkInOut++; + } + *pWalk = 0; // null terminator + + pBufInOut.Adopt(pBuf); + } +} + +/** + * Determines the MIME type, if present, from the current line. + * + * m_partIsHtml, m_isMultipart, m_partIsText, m_base64part, and boundary are + * all set by this method at various points in time. + * + * @param line (in) a header line that may contain a MIME header + */ +void nsMsgBodyHandler::SniffPossibleMIMEHeader(const nsCString& line) { + // Some parts of MIME are case-sensitive and other parts are case-insensitive; + // specifically, the headers are all case-insensitive and the values we care + // about are also case-insensitive, with the sole exception of the boundary + // string, so we can't just take the input line and make it lower case. + nsCString lowerCaseLine(line); + ToLowerCase(lowerCaseLine); + + if (StringBeginsWith(lowerCaseLine, "content-transfer-encoding:"_ns)) + m_partIsQP = lowerCaseLine.Find("quoted-printable") != kNotFound; + + if (StringBeginsWith(lowerCaseLine, "content-type:"_ns)) { + if (lowerCaseLine.LowerCaseFindASCII("text/html") != kNotFound) { + m_partIsText = true; + m_partIsHtml = true; + } else if (lowerCaseLine.Find("multipart/") != kNotFound) { + if (m_isMultipart) { + // Nested multipart, get ready for new headers. + m_base64part = false; + m_partIsQP = false; + m_pastPartHeaders = false; + m_partIsHtml = false; + m_partIsText = false; + } + m_isMultipart = true; + m_partCharset.Truncate(); + } else if (lowerCaseLine.Find("message/") != kNotFound) { + // Initialise again. + m_base64part = false; + m_partIsQP = false; + m_pastPartHeaders = false; + m_partIsHtml = false; + m_partIsText = + true; // Default is text/plain, maybe proven otherwise later. + m_inMessageAttachment = true; + } else if (lowerCaseLine.Find("text/") != kNotFound) + m_partIsText = true; + else if (lowerCaseLine.Find("text/") == kNotFound) + m_partIsText = false; // We have disproven our assumption. + } + + int32_t start; + if (m_isMultipart && (start = lowerCaseLine.Find("boundary=")) != kNotFound) { + start += 9; // strlen("boundary=") + if (line[start] == '\"') start++; + int32_t end = line.RFindChar('\"'); + if (end == -1) end = line.Length(); + + // Collect all boundaries. Since we only react to crossing a boundary, + // we can simply collect the boundaries instead of forming a tree + // structure from the message. Keep it simple ;-) + nsCString boundary; + boundary.AssignLiteral("--"); + boundary.Append(Substring(line, start, end - start)); + if (!m_boundaries.Contains(boundary)) m_boundaries.AppendElement(boundary); + } + + if (m_isMultipart && (start = lowerCaseLine.Find("charset=")) != kNotFound) { + start += 8; // strlen("charset=") + bool foundQuote = false; + if (line[start] == '\"') { + start++; + foundQuote = true; + } + int32_t end = line.FindChar(foundQuote ? '\"' : ';', start); + if (end == -1) end = line.Length(); + + m_partCharset.Assign(Substring(line, start, end - start)); + } + + if (StringBeginsWith(lowerCaseLine, "content-transfer-encoding:"_ns) && + lowerCaseLine.LowerCaseFindASCII(ENCODING_BASE64) != kNotFound) + m_base64part = true; +} + +/** + * Decodes the given base64 string. + * + * It returns its decoded string in its input. + * + * @param pBufInOut (inout) a buffer of the string + */ +void nsMsgBodyHandler::Base64Decode(nsCString& pBufInOut) { + char* decodedBody = + PL_Base64Decode(pBufInOut.get(), pBufInOut.Length(), nullptr); + if (decodedBody) { + // Replace CR LF with spaces. + char* q = decodedBody; + while (*q) { + if (*q == '\n' || *q == '\r') *q = ' '; + q++; + } + pBufInOut.Adopt(decodedBody); + } +} diff --git a/comm/mailnews/search/src/nsMsgFilter.cpp b/comm/mailnews/search/src/nsMsgFilter.cpp new file mode 100644 index 0000000000..273b74aa07 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgFilter.cpp @@ -0,0 +1,864 @@ +/* -*- Mode: C++; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// this file implements the nsMsgFilter interface + +#include "msgCore.h" +#include "nsIMsgHdr.h" +#include "nsMsgFilterList.h" // for kFileVersion +#include "nsMsgFilter.h" +#include "nsMsgUtils.h" +#include "nsMsgLocalSearch.h" +#include "nsMsgSearchTerm.h" +#include "nsIMsgAccountManager.h" +#include "nsIMsgIncomingServer.h" +#include "nsMsgSearchValue.h" +#include "nsMsgI18N.h" +#include "nsNativeCharsetUtils.h" +#include "nsIOutputStream.h" +#include "nsIStringBundle.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsIMsgFilterService.h" +#include "nsIMsgNewsFolder.h" +#include "prmem.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Components.h" +#include "mozilla/intl/AppDateTimeFormat.h" + +static const char* kImapPrefix = "//imap:"; +static const char* kWhitespace = "\b\t\r\n "; + +nsMsgRuleAction::nsMsgRuleAction() + : m_type(nsMsgFilterAction::None), + m_priority(nsMsgPriority::notSet), + m_junkScore(0) {} + +nsMsgRuleAction::~nsMsgRuleAction() {} + +NS_IMPL_ISUPPORTS(nsMsgRuleAction, nsIMsgRuleAction) + +NS_IMPL_GETSET(nsMsgRuleAction, Type, nsMsgRuleActionType, m_type) + +NS_IMETHODIMP nsMsgRuleAction::SetPriority(nsMsgPriorityValue aPriority) { + NS_ENSURE_TRUE(m_type == nsMsgFilterAction::ChangePriority, + NS_ERROR_ILLEGAL_VALUE); + m_priority = aPriority; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgRuleAction::GetPriority(nsMsgPriorityValue* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_ENSURE_TRUE(m_type == nsMsgFilterAction::ChangePriority, + NS_ERROR_ILLEGAL_VALUE); + *aResult = m_priority; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgRuleAction::SetTargetFolderUri(const nsACString& aUri) { + NS_ENSURE_TRUE(m_type == nsMsgFilterAction::MoveToFolder || + m_type == nsMsgFilterAction::CopyToFolder, + NS_ERROR_ILLEGAL_VALUE); + m_folderUri = aUri; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgRuleAction::GetTargetFolderUri(nsACString& aResult) { + NS_ENSURE_TRUE(m_type == nsMsgFilterAction::MoveToFolder || + m_type == nsMsgFilterAction::CopyToFolder, + NS_ERROR_ILLEGAL_VALUE); + aResult = m_folderUri; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgRuleAction::SetJunkScore(int32_t aJunkScore) { + NS_ENSURE_TRUE(m_type == nsMsgFilterAction::JunkScore && aJunkScore >= 0 && + aJunkScore <= 100, + NS_ERROR_ILLEGAL_VALUE); + m_junkScore = aJunkScore; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgRuleAction::GetJunkScore(int32_t* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_ENSURE_TRUE(m_type == nsMsgFilterAction::JunkScore, + NS_ERROR_ILLEGAL_VALUE); + *aResult = m_junkScore; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgRuleAction::SetStrValue(const nsACString& aStrValue) { + m_strValue = aStrValue; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgRuleAction::GetStrValue(nsACString& aStrValue) { + aStrValue = m_strValue; + return NS_OK; +} + +/* attribute ACString customId; */ +NS_IMETHODIMP nsMsgRuleAction::GetCustomId(nsACString& aCustomId) { + aCustomId = m_customId; + return NS_OK; +} + +NS_IMETHODIMP nsMsgRuleAction::SetCustomId(const nsACString& aCustomId) { + m_customId = aCustomId; + return NS_OK; +} + +// this can only be called after the customId is set +NS_IMETHODIMP nsMsgRuleAction::GetCustomAction( + nsIMsgFilterCustomAction** aCustomAction) { + NS_ENSURE_ARG_POINTER(aCustomAction); + if (!m_customAction) { + if (m_customId.IsEmpty()) return NS_ERROR_NOT_INITIALIZED; + nsresult rv; + nsCOMPtr<nsIMsgFilterService> filterService = + do_GetService("@mozilla.org/messenger/services/filters;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = filterService->GetCustomAction(m_customId, + getter_AddRefs(m_customAction)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // found the correct custom action + NS_ADDREF(*aCustomAction = m_customAction); + return NS_OK; +} + +nsMsgFilter::nsMsgFilter() + : m_type(nsMsgFilterType::InboxRule | nsMsgFilterType::Manual), + m_enabled(false), + m_temporary(false), + m_unparseable(false), + m_filterList(nullptr), + m_expressionTree(nullptr) {} + +nsMsgFilter::~nsMsgFilter() { delete m_expressionTree; } + +NS_IMPL_ISUPPORTS(nsMsgFilter, nsIMsgFilter) + +NS_IMPL_GETSET(nsMsgFilter, FilterType, nsMsgFilterTypeType, m_type) +NS_IMPL_GETSET(nsMsgFilter, Enabled, bool, m_enabled) +NS_IMPL_GETSET(nsMsgFilter, Temporary, bool, m_temporary) +NS_IMPL_GETSET(nsMsgFilter, Unparseable, bool, m_unparseable) + +NS_IMETHODIMP nsMsgFilter::GetFilterName(nsAString& name) { + name = m_filterName; + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilter::SetFilterName(const nsAString& name) { + m_filterName.Assign(name); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilter::GetFilterDesc(nsACString& description) { + description = m_description; + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilter::SetFilterDesc(const nsACString& description) { + m_description.Assign(description); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilter::GetUnparsedBuffer(nsACString& unparsedBuffer) { + unparsedBuffer = m_unparsedBuffer; + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilter::SetUnparsedBuffer(const nsACString& unparsedBuffer) { + m_unparsedBuffer.Assign(unparsedBuffer); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilter::AddTerm( + nsMsgSearchAttribValue attrib, /* attribute for this term */ + nsMsgSearchOpValue op, /* operator e.g. opContains */ + nsIMsgSearchValue* value, /* value e.g. "Dogbert" */ + bool BooleanAND, /* true if AND is the boolean operator. + false if OR is the boolean operators */ + const nsACString& arbitraryHeader) /* arbitrary header specified by user. + ignored unless attrib = attribOtherHeader */ +{ + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilter::AppendTerm(nsIMsgSearchTerm* aTerm) { + NS_ENSURE_TRUE(aTerm, NS_ERROR_NULL_POINTER); + // invalidate expression tree if we're changing the terms + delete m_expressionTree; + m_expressionTree = nullptr; + m_termList.AppendElement(aTerm); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilter::CreateTerm(nsIMsgSearchTerm** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_ADDREF(*aResult = new nsMsgSearchTerm); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilter::CreateAction(nsIMsgRuleAction** aAction) { + NS_ENSURE_ARG_POINTER(aAction); + NS_ADDREF(*aAction = new nsMsgRuleAction); + return NS_OK; +} + +// All the rules' actions form a unit, with no real order imposed. +// But certain actions like MoveToFolder or StopExecution would make us drop +// consecutive actions, while actions like AddTag implicitly care about the +// order of invocation. Hence we do as little reordering as possible, keeping +// the user-defined order as much as possible. +// We explicitly don't allow for filters which do "tag message as Important, +// copy it to another folder, tag it as To Do also, copy this different state +// elsewhere" in one go. You need to define separate filters for that. +// +// The order of actions returned by this method: +// index action(s) +// ------- --------- +// 0 FetchBodyFromPop3Server +// 1..n all other 'normal' actions, in their original order +// n+1..m CopyToFolder +// m+1 MoveToFolder or Delete +// m+2 StopExecution +NS_IMETHODIMP +nsMsgFilter::GetSortedActionList( + nsTArray<RefPtr<nsIMsgRuleAction>>& aActionList) { + aActionList.Clear(); + aActionList.SetCapacity(m_actionList.Length()); + + // hold separate pointers into the action list + uint32_t nextIndexForNormal = 0, nextIndexForCopy = 0, nextIndexForMove = 0; + for (auto action : m_actionList) { + if (!action) continue; + + nsMsgRuleActionType actionType; + action->GetType(&actionType); + switch (actionType) { + case nsMsgFilterAction::FetchBodyFromPop3Server: { + // always insert in front + aActionList.InsertElementAt(0, action); + ++nextIndexForNormal; + ++nextIndexForCopy; + ++nextIndexForMove; + break; + } + + case nsMsgFilterAction::CopyToFolder: { + // insert into copy actions block, in order of appearance + aActionList.InsertElementAt(nextIndexForCopy, action); + ++nextIndexForCopy; + ++nextIndexForMove; + break; + } + + case nsMsgFilterAction::MoveToFolder: + case nsMsgFilterAction::Delete: { + // insert into move/delete action block + aActionList.InsertElementAt(nextIndexForMove, action); + ++nextIndexForMove; + break; + } + + case nsMsgFilterAction::StopExecution: { + // insert into stop action block + aActionList.AppendElement(action); + break; + } + + default: { + // insert into normal action block, in order of appearance + aActionList.InsertElementAt(nextIndexForNormal, action); + ++nextIndexForNormal; + ++nextIndexForCopy; + ++nextIndexForMove; + break; + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilter::AppendAction(nsIMsgRuleAction* aAction) { + NS_ENSURE_ARG_POINTER(aAction); + + m_actionList.AppendElement(aAction); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilter::GetActionAt(uint32_t aIndex, nsIMsgRuleAction** aAction) { + NS_ENSURE_ARG_POINTER(aAction); + NS_ENSURE_ARG(aIndex < m_actionList.Length()); + + NS_ENSURE_TRUE(m_actionList[aIndex], NS_ERROR_ILLEGAL_VALUE); + NS_IF_ADDREF(*aAction = m_actionList[aIndex]); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilter::GetActionIndex(nsIMsgRuleAction* aAction, int32_t* aIndex) { + NS_ENSURE_ARG_POINTER(aIndex); + + *aIndex = m_actionList.IndexOf(aAction); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilter::GetActionCount(uint32_t* aCount) { + NS_ENSURE_ARG_POINTER(aCount); + + *aCount = m_actionList.Length(); + return NS_OK; +} + +NS_IMETHODIMP // for editing a filter +nsMsgFilter::ClearActionList() { + m_actionList.Clear(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilter::GetTerm( + int32_t termIndex, + nsMsgSearchAttribValue* attrib, /* attribute for this term */ + nsMsgSearchOpValue* op, /* operator e.g. opContains */ + nsIMsgSearchValue** value, /* value e.g. "Dogbert" */ + bool* booleanAnd, /* true if AND is the boolean operator. false if OR is the + boolean operator */ + nsACString& arbitraryHeader) /* arbitrary header specified by user.ignore + unless attrib = attribOtherHeader */ +{ + if (termIndex >= (int32_t)m_termList.Length()) { + return NS_ERROR_INVALID_ARG; + } + nsIMsgSearchTerm* term = m_termList[termIndex]; + if (attrib) term->GetAttrib(attrib); + if (op) term->GetOp(op); + if (value) term->GetValue(value); + if (booleanAnd) term->GetBooleanAnd(booleanAnd); + if (attrib && *attrib > nsMsgSearchAttrib::OtherHeader && + *attrib < nsMsgSearchAttrib::kNumMsgSearchAttributes) { + term->GetArbitraryHeader(arbitraryHeader); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilter::GetSearchTerms( + nsTArray<RefPtr<nsIMsgSearchTerm>>& terms) { + delete m_expressionTree; + m_expressionTree = nullptr; + terms = m_termList.Clone(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilter::SetSearchTerms( + nsTArray<RefPtr<nsIMsgSearchTerm>> const& terms) { + delete m_expressionTree; + m_expressionTree = nullptr; + m_termList = terms.Clone(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilter::SetScope(nsIMsgSearchScopeTerm* aResult) { + m_scope = aResult; + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilter::GetScope(nsIMsgSearchScopeTerm** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_IF_ADDREF(*aResult = m_scope); + return NS_OK; +} + +// This function handles the logging both for success of filtering +// (NS_SUCCEEDED(aRcode)), and for error reporting (NS_FAILED(aRcode) +// when the filter action (such as file move/copy) failed. +// +// @param aRcode NS_OK for successful filtering +// operation, otherwise, an error code for filtering failure. +// @param aErrmsg Not used for success case (ignored), and a non-null +// error message for failure case. +// +// CAUTION: Unless logging is enabled, no error/warning is shown. +// So enable logging if you would like to see the error/warning. +// +// XXX The current code in this file does not report errors of minor +// operations such as adding labels and so forth which may fail when +// underlying file system for the message store experiences +// failure. For now, most visible major errors such as message +// move/copy failures are taken care of. +// +// XXX Possible Improvement: For error case reporting, someone might +// want to implement a transient message that appears and stick until +// the user clears in the message status bar, etc. For now, we log an +// error in a similar form as a conventional successful filter event +// with additional error information at the beginning. +// +nsresult nsMsgFilter::LogRuleHitGeneric(nsIMsgRuleAction* aFilterAction, + nsIMsgDBHdr* aMsgHdr, nsresult aRcode, + const nsACString& aErrmsg) { + NS_ENSURE_ARG_POINTER(aFilterAction); + NS_ENSURE_ARG_POINTER(aMsgHdr); + + NS_ENSURE_TRUE(m_filterList, NS_OK); + + PRTime date; + nsMsgRuleActionType actionType; + + nsString authorValue; + nsString subjectValue; + nsString filterName; + nsString dateValue; + + GetFilterName(filterName); + aFilterAction->GetType(&actionType); + (void)aMsgHdr->GetDate(&date); + PRExplodedTime exploded; + PR_ExplodeTime(date, PR_LocalTimeParameters, &exploded); + + mozilla::intl::DateTimeFormat::StyleBag style; + style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short); + style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long); + mozilla::intl::AppDateTimeFormat::Format(style, &exploded, dateValue); + + (void)aMsgHdr->GetMime2DecodedAuthor(authorValue); + (void)aMsgHdr->GetMime2DecodedSubject(subjectValue); + + nsString buffer; + // this is big enough to hold a log entry. + // do this so we avoid growing and copying as we append to the log. + buffer.SetCapacity(512); + + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv = bundleService->CreateBundle( + "chrome://messenger/locale/filter.properties", getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + // If error, prefix with the error code and error message. + // A desired wording (without NEWLINEs): + // Filter Action Failed "Move failed" with error code=0x80004005 + // while attempting: Applied filter "test" to message from + // Some Test <test@example.com> - send test 3 at 2/13/2015 11:32:53 AM + // moved message id = 54DE5165.7000907@example.com to + // mailbox://nobody@Local%20Folders/test + if (NS_FAILED(aRcode)) { + // Convert aErrmsg to UTF16 string, and + // convert aRcode to UTF16 string in advance. + char tcode[20]; + PR_snprintf(tcode, sizeof(tcode), "0x%08x", aRcode); + NS_ConvertASCIItoUTF16 tcode16(tcode); + + nsString tErrmsg; + if (actionType != nsMsgFilterAction::Custom) { + // If this is one of our internal actions, the passed string + // is an identifier to get from the bundle. + rv = + bundle->GetStringFromName(PromiseFlatCString(aErrmsg).get(), tErrmsg); + if (NS_FAILED(rv)) tErrmsg.Assign(NS_ConvertUTF8toUTF16(aErrmsg)); + } else { + // The addon creating the custom action should have passed a localized + // string. + tErrmsg.Assign(NS_ConvertUTF8toUTF16(aErrmsg)); + } + AutoTArray<nsString, 2> logErrorFormatStrings = {tErrmsg, tcode16}; + + nsString filterFailureWarningPrefix; + rv = bundle->FormatStringFromName("filterFailureWarningPrefix", + logErrorFormatStrings, + filterFailureWarningPrefix); + NS_ENSURE_SUCCESS(rv, rv); + buffer += filterFailureWarningPrefix; + buffer.AppendLiteral("\n"); + } + + AutoTArray<nsString, 4> filterLogDetectFormatStrings = { + filterName, authorValue, subjectValue, dateValue}; + nsString filterLogDetectStr; + rv = bundle->FormatStringFromName( + "filterLogDetectStr", filterLogDetectFormatStrings, filterLogDetectStr); + NS_ENSURE_SUCCESS(rv, rv); + + buffer += filterLogDetectStr; + buffer.AppendLiteral("\n"); + + if (actionType == nsMsgFilterAction::MoveToFolder || + actionType == nsMsgFilterAction::CopyToFolder) { + nsCString actionFolderUri; + aFilterAction->GetTargetFolderUri(actionFolderUri); + + nsCString msgId; + aMsgHdr->GetMessageId(getter_Copies(msgId)); + + AutoTArray<nsString, 2> logMoveFormatStrings; + CopyUTF8toUTF16(msgId, *logMoveFormatStrings.AppendElement()); + CopyUTF8toUTF16(actionFolderUri, *logMoveFormatStrings.AppendElement()); + nsString logMoveStr; + rv = bundle->FormatStringFromName( + (actionType == nsMsgFilterAction::MoveToFolder) ? "logMoveStr" + : "logCopyStr", + logMoveFormatStrings, logMoveStr); + NS_ENSURE_SUCCESS(rv, rv); + + buffer += logMoveStr; + } else if (actionType == nsMsgFilterAction::Custom) { + nsCOMPtr<nsIMsgFilterCustomAction> customAction; + nsAutoString filterActionName; + rv = aFilterAction->GetCustomAction(getter_AddRefs(customAction)); + if (NS_SUCCEEDED(rv) && customAction) + customAction->GetName(filterActionName); + if (filterActionName.IsEmpty()) + bundle->GetStringFromName("filterMissingCustomAction", filterActionName); + buffer += filterActionName; + } else { + nsString actionValue; + nsAutoCString filterActionID; + filterActionID = "filterAction"_ns; + filterActionID.AppendInt(actionType); + rv = bundle->GetStringFromName(filterActionID.get(), actionValue); + NS_ENSURE_SUCCESS(rv, rv); + + buffer += actionValue; + } + buffer.AppendLiteral("\n"); + + return m_filterList->LogFilterMessage(buffer, nullptr); +} + +NS_IMETHODIMP nsMsgFilter::LogRuleHit(nsIMsgRuleAction* aFilterAction, + nsIMsgDBHdr* aMsgHdr) { + return nsMsgFilter::LogRuleHitGeneric(aFilterAction, aMsgHdr, NS_OK, + EmptyCString()); +} + +NS_IMETHODIMP nsMsgFilter::LogRuleHitFail(nsIMsgRuleAction* aFilterAction, + nsIMsgDBHdr* aMsgHdr, nsresult aRcode, + const nsACString& aErrMsg) { + return nsMsgFilter::LogRuleHitGeneric(aFilterAction, aMsgHdr, aRcode, + aErrMsg); +} + +NS_IMETHODIMP +nsMsgFilter::MatchHdr(nsIMsgDBHdr* msgHdr, nsIMsgFolder* folder, + nsIMsgDatabase* db, const nsACString& headers, + bool* pResult) { + NS_ENSURE_ARG_POINTER(folder); + NS_ENSURE_ARG_POINTER(msgHdr); + nsCString folderCharset = "UTF-8"_ns; + nsCOMPtr<nsIMsgNewsFolder> newsfolder(do_QueryInterface(folder)); + if (newsfolder) newsfolder->GetCharset(folderCharset); + return nsMsgSearchOfflineMail::MatchTermsForFilter( + msgHdr, m_termList, folderCharset.get(), m_scope, db, headers, + &m_expressionTree, pResult); +} + +NS_IMETHODIMP +nsMsgFilter::SetFilterList(nsIMsgFilterList* filterList) { + // doesn't hold a ref. + m_filterList = filterList; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilter::GetFilterList(nsIMsgFilterList** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_IF_ADDREF(*aResult = m_filterList); + return NS_OK; +} + +void nsMsgFilter::SetFilterScript(nsCString* fileName) { + m_scriptFileName = *fileName; +} + +nsresult nsMsgFilter::ConvertMoveOrCopyToFolderValue( + nsIMsgRuleAction* filterAction, nsCString& moveValue) { + NS_ENSURE_ARG_POINTER(filterAction); + int16_t filterVersion = kFileVersion; + if (m_filterList) m_filterList->GetVersion(&filterVersion); + if (filterVersion <= k60Beta1Version) { + nsCOMPtr<nsIMsgFolder> rootFolder; + nsCString folderUri; + + m_filterList->GetFolder(getter_AddRefs(rootFolder)); + // if relative path starts with kImap, this is a move to folder on the same + // server + if (moveValue.Find(kImapPrefix) == 0) { + int32_t prefixLen = PL_strlen(kImapPrefix); + nsAutoCString originalServerPath(Substring(moveValue, prefixLen)); + if (filterVersion == k45Version) { + nsAutoString unicodeStr; + NS_CopyNativeToUnicode(originalServerPath, unicodeStr); + + nsresult rv = CopyUTF16toMUTF7(unicodeStr, originalServerPath); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIMsgFolder> destIFolder; + if (rootFolder) { + rootFolder->FindSubFolder(originalServerPath, + getter_AddRefs(destIFolder)); + if (destIFolder) { + destIFolder->GetURI(folderUri); + filterAction->SetTargetFolderUri(folderUri); + moveValue.Assign(folderUri); + } + } + } else { + // start off leaving the value the same. + filterAction->SetTargetFolderUri(moveValue); + nsresult rv = NS_OK; + nsCOMPtr<nsIMsgFolder> localMailRoot; + rootFolder->GetURI(folderUri); + // if the root folder is not imap, than the local mail root is the server + // root. otherwise, it's the migrated local folders. + if (!StringBeginsWith(folderUri, "imap:"_ns)) + localMailRoot = rootFolder; + else { + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgIncomingServer> server; + rv = accountManager->GetLocalFoldersServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv) && server) + rv = server->GetRootFolder(getter_AddRefs(localMailRoot)); + } + if (NS_SUCCEEDED(rv) && localMailRoot) { + nsCString localRootURI; + nsCOMPtr<nsIMsgFolder> destIMsgFolder; + localMailRoot->GetURI(localRootURI); + nsCString destFolderUri; + destFolderUri.Assign(localRootURI); + // need to remove ".sbd" from moveValue, and perhaps escape it. + int32_t offset = moveValue.Find(FOLDER_SUFFIX8 "/"); + if (offset != -1) moveValue.Cut(offset, FOLDER_SUFFIX_LENGTH); + +#ifdef XP_MACOSX + nsCString unescapedMoveValue; + MsgUnescapeString(moveValue, 0, unescapedMoveValue); + moveValue = unescapedMoveValue; +#endif + destFolderUri.Append('/'); + if (filterVersion == k45Version) { + nsAutoString unicodeStr; + NS_CopyNativeToUnicode(moveValue, unicodeStr); + rv = NS_MsgEscapeEncodeURLPath(unicodeStr, moveValue); + } + destFolderUri.Append(moveValue); + localMailRoot->GetChildWithURI(destFolderUri, true, + false /*caseInsensitive*/, + getter_AddRefs(destIMsgFolder)); + + if (destIMsgFolder) { + destIMsgFolder->GetURI(folderUri); + filterAction->SetTargetFolderUri(folderUri); + moveValue.Assign(folderUri); + } + } + } + } else + filterAction->SetTargetFolderUri(moveValue); + + return NS_OK; + // set m_action.m_value.m_folderUri +} + +NS_IMETHODIMP +nsMsgFilter::SaveToTextFile(nsIOutputStream* aStream) { + NS_ENSURE_ARG_POINTER(aStream); + if (m_unparseable) { + uint32_t bytesWritten; + // we need to trim leading whitespaces before filing out + m_unparsedBuffer.Trim(kWhitespace, true /*leadingCharacters*/, + false /*trailingCharacters*/); + return aStream->Write(m_unparsedBuffer.get(), m_unparsedBuffer.Length(), + &bytesWritten); + } + nsresult err = m_filterList->WriteWstrAttr(nsIMsgFilterList::attribName, + m_filterName.get(), aStream); + err = m_filterList->WriteBoolAttr(nsIMsgFilterList::attribEnabled, m_enabled, + aStream); + err = m_filterList->WriteStrAttr(nsIMsgFilterList::attribDescription, + m_description.get(), aStream); + err = + m_filterList->WriteIntAttr(nsIMsgFilterList::attribType, m_type, aStream); + if (IsScript()) + err = m_filterList->WriteStrAttr(nsIMsgFilterList::attribScriptFile, + m_scriptFileName.get(), aStream); + else + err = SaveRule(aStream); + return err; +} + +nsresult nsMsgFilter::SaveRule(nsIOutputStream* aStream) { + nsresult err = NS_OK; + nsCOMPtr<nsIMsgFilterList> filterList; + GetFilterList(getter_AddRefs(filterList)); + nsAutoCString actionFilingStr; + + uint32_t numActions; + err = GetActionCount(&numActions); + NS_ENSURE_SUCCESS(err, err); + + for (uint32_t index = 0; index < numActions; index++) { + nsCOMPtr<nsIMsgRuleAction> action; + err = GetActionAt(index, getter_AddRefs(action)); + if (NS_FAILED(err) || !action) continue; + + nsMsgRuleActionType actionType; + action->GetType(&actionType); + GetActionFilingStr(actionType, actionFilingStr); + + err = filterList->WriteStrAttr(nsIMsgFilterList::attribAction, + actionFilingStr.get(), aStream); + NS_ENSURE_SUCCESS(err, err); + + switch (actionType) { + case nsMsgFilterAction::MoveToFolder: + case nsMsgFilterAction::CopyToFolder: { + nsCString imapTargetString; + action->GetTargetFolderUri(imapTargetString); + err = filterList->WriteStrAttr(nsIMsgFilterList::attribActionValue, + imapTargetString.get(), aStream); + } break; + case nsMsgFilterAction::ChangePriority: { + nsMsgPriorityValue priorityValue; + action->GetPriority(&priorityValue); + nsAutoCString priority; + NS_MsgGetUntranslatedPriorityName(priorityValue, priority); + err = filterList->WriteStrAttr(nsIMsgFilterList::attribActionValue, + priority.get(), aStream); + } break; + case nsMsgFilterAction::JunkScore: { + int32_t junkScore; + action->GetJunkScore(&junkScore); + err = filterList->WriteIntAttr(nsIMsgFilterList::attribActionValue, + junkScore, aStream); + } break; + case nsMsgFilterAction::AddTag: + case nsMsgFilterAction::Reply: + case nsMsgFilterAction::Forward: { + nsCString strValue; + action->GetStrValue(strValue); + // strValue is e-mail address + err = filterList->WriteStrAttr(nsIMsgFilterList::attribActionValue, + strValue.get(), aStream); + } break; + case nsMsgFilterAction::Custom: { + nsAutoCString id; + action->GetCustomId(id); + err = filterList->WriteStrAttr(nsIMsgFilterList::attribCustomId, + id.get(), aStream); + nsAutoCString strValue; + action->GetStrValue(strValue); + if (strValue.Length()) + err = filterList->WriteWstrAttr(nsIMsgFilterList::attribActionValue, + NS_ConvertUTF8toUTF16(strValue).get(), + aStream); + } break; + + default: + break; + } + NS_ENSURE_SUCCESS(err, err); + } + // and here the fun begins - file out term list... + nsAutoCString condition; + err = MsgTermListToString(m_termList, condition); + NS_ENSURE_SUCCESS(err, err); + return filterList->WriteStrAttr(nsIMsgFilterList::attribCondition, + condition.get(), aStream); +} + +// for each action, this table encodes the filterTypes that support the action. +struct RuleActionsTableEntry { + nsMsgRuleActionType action; + const char* + actionFilingStr; /* used for filing out filters, don't translate! */ +}; + +static struct RuleActionsTableEntry ruleActionsTable[] = { + {nsMsgFilterAction::MoveToFolder, "Move to folder"}, + {nsMsgFilterAction::CopyToFolder, "Copy to folder"}, + {nsMsgFilterAction::ChangePriority, "Change priority"}, + {nsMsgFilterAction::Delete, "Delete"}, + {nsMsgFilterAction::MarkRead, "Mark read"}, + {nsMsgFilterAction::KillThread, "Ignore thread"}, + {nsMsgFilterAction::KillSubthread, "Ignore subthread"}, + {nsMsgFilterAction::WatchThread, "Watch thread"}, + {nsMsgFilterAction::MarkFlagged, "Mark flagged"}, + {nsMsgFilterAction::Reply, "Reply"}, + {nsMsgFilterAction::Forward, "Forward"}, + {nsMsgFilterAction::StopExecution, "Stop execution"}, + {nsMsgFilterAction::DeleteFromPop3Server, "Delete from Pop3 server"}, + {nsMsgFilterAction::LeaveOnPop3Server, "Leave on Pop3 server"}, + {nsMsgFilterAction::JunkScore, "JunkScore"}, + {nsMsgFilterAction::FetchBodyFromPop3Server, "Fetch body from Pop3Server"}, + {nsMsgFilterAction::AddTag, "AddTag"}, + {nsMsgFilterAction::MarkUnread, "Mark unread"}, + {nsMsgFilterAction::Custom, "Custom"}, +}; + +static const unsigned int sNumActions = MOZ_ARRAY_LENGTH(ruleActionsTable); + +const char* nsMsgFilter::GetActionStr(nsMsgRuleActionType action) { + for (unsigned int i = 0; i < sNumActions; i++) { + if (action == ruleActionsTable[i].action) + return ruleActionsTable[i].actionFilingStr; + } + return ""; +} +/*static */ nsresult nsMsgFilter::GetActionFilingStr(nsMsgRuleActionType action, + nsCString& actionStr) { + for (unsigned int i = 0; i < sNumActions; i++) { + if (action == ruleActionsTable[i].action) { + actionStr = ruleActionsTable[i].actionFilingStr; + return NS_OK; + } + } + return NS_ERROR_INVALID_ARG; +} + +nsMsgRuleActionType nsMsgFilter::GetActionForFilingStr(nsCString& actionStr) { + for (unsigned int i = 0; i < sNumActions; i++) { + if (actionStr.Equals(ruleActionsTable[i].actionFilingStr)) + return ruleActionsTable[i].action; + } + return nsMsgFilterAction::None; +} + +int16_t nsMsgFilter::GetVersion() { + if (!m_filterList) return 0; + int16_t version; + m_filterList->GetVersion(&version); + return version; +} + +#ifdef DEBUG +void nsMsgFilter::Dump() { + nsAutoCString s; + LossyCopyUTF16toASCII(m_filterName, s); + printf("filter %s type = %c desc = %s\n", s.get(), m_type + '0', + m_description.get()); +} +#endif diff --git a/comm/mailnews/search/src/nsMsgFilter.h b/comm/mailnews/search/src/nsMsgFilter.h new file mode 100644 index 0000000000..bf77f7a992 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgFilter.h @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgFilter_H_ +#define _nsMsgFilter_H_ + +#include "nscore.h" +#include "nsISupports.h" +#include "nsIMsgFilter.h" +#include "nsIMsgSearchScopeTerm.h" +#include "nsMsgSearchBoolExpression.h" +#include "nsIMsgFilterCustomAction.h" + +class nsMsgRuleAction : public nsIMsgRuleAction { + public: + NS_DECL_ISUPPORTS + + nsMsgRuleAction(); + + NS_DECL_NSIMSGRULEACTION + + private: + virtual ~nsMsgRuleAction(); + + nsMsgRuleActionType m_type; + // this used to be a union - why bother? + nsMsgPriorityValue m_priority; /* priority to set rule to */ + nsCString m_folderUri; + int32_t m_junkScore; /* junk score (or arbitrary int value?) */ + // arbitrary string value. Currently, email address to forward to + nsCString m_strValue; + nsCString m_customId; + nsCOMPtr<nsIMsgFilterCustomAction> m_customAction; +}; + +class nsMsgFilter : public nsIMsgFilter { + public: + NS_DECL_ISUPPORTS + + nsMsgFilter(); + + NS_DECL_NSIMSGFILTER + + nsMsgFilterTypeType GetType() { return m_type; } + void SetType(nsMsgFilterTypeType type) { m_type = type; } + bool GetEnabled() { return m_enabled; } + void SetFilterScript(nsCString* filterName); + + bool IsScript() { + return (m_type & (nsMsgFilterType::InboxJavaScript | + nsMsgFilterType::NewsJavaScript)) != 0; + } + + // filing routines. + nsresult SaveRule(nsIOutputStream* aStream); + + int16_t GetVersion(); +#ifdef DEBUG + void Dump(); +#endif + + nsresult ConvertMoveOrCopyToFolderValue(nsIMsgRuleAction* filterAction, + nsCString& relativePath); + static const char* GetActionStr(nsMsgRuleActionType action); + static nsresult GetActionFilingStr(nsMsgRuleActionType action, + nsCString& actionStr); + static nsMsgRuleActionType GetActionForFilingStr(nsCString& actionStr); + + protected: + /* + * Reporting function for filtering success/failure. + * Logging has to be enabled for the message to appear. + */ + nsresult LogRuleHitGeneric(nsIMsgRuleAction* aFilterAction, + nsIMsgDBHdr* aMsgHdr, nsresult aRcode, + const nsACString& aErrmsg); + + virtual ~nsMsgFilter(); + + nsMsgFilterTypeType m_type; + nsString m_filterName; + nsCString m_scriptFileName; // iff this filter is a script. + nsCString m_description; + nsCString m_unparsedBuffer; + + bool m_enabled; + bool m_temporary; + bool m_unparseable; + nsIMsgFilterList* m_filterList; /* owning filter list */ + nsTArray<RefPtr<nsIMsgSearchTerm>> m_termList; /* criteria terms */ + nsCOMPtr<nsIMsgSearchScopeTerm> + m_scope; /* default for mail rules is inbox, but news rules could + have a newsgroup - LDAP would be invalid */ + nsTArray<nsCOMPtr<nsIMsgRuleAction>> m_actionList; + nsMsgSearchBoolExpression* m_expressionTree; +}; + +#endif diff --git a/comm/mailnews/search/src/nsMsgFilterList.cpp b/comm/mailnews/search/src/nsMsgFilterList.cpp new file mode 100644 index 0000000000..4a81eafe09 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgFilterList.cpp @@ -0,0 +1,1207 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// this file implements the nsMsgFilterList interface + +#include "nsTextFormatter.h" + +#include "msgCore.h" +#include "nsMsgFilterList.h" +#include "nsMsgFilter.h" +#include "nsIMsgHdr.h" +#include "nsIMsgFilterHitNotify.h" +#include "nsMsgUtils.h" +#include "nsMsgSearchTerm.h" +#include "nsString.h" +#include "nsIMsgFilterService.h" +#include "nsMsgSearchScopeTerm.h" +#include "nsIStringBundle.h" +#include "nsNetUtil.h" +#include "nsIInputStream.h" +#include "nsNativeCharsetUtils.h" +#include "nsMemory.h" +#include "prmem.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Components.h" +#include "mozilla/Logging.h" +#include "mozilla/intl/AppDateTimeFormat.h" +#include <ctype.h> + +// Marker for EOF or failure during read +#define EOF_CHAR -1 + +using namespace mozilla; + +extern LazyLogModule FILTERLOGMODULE; + +static uint32_t nextListId = 0; + +nsMsgFilterList::nsMsgFilterList() : m_fileVersion(0) { + m_loggingEnabled = false; + m_startWritingToBuffer = false; + m_temporaryList = false; + m_curFilter = nullptr; + m_listId.Assign("List"); + m_listId.AppendInt(nextListId++); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Creating a new filter list with id=%s", m_listId.get())); +} + +NS_IMPL_ADDREF(nsMsgFilterList) +NS_IMPL_RELEASE(nsMsgFilterList) +NS_IMPL_QUERY_INTERFACE(nsMsgFilterList, nsIMsgFilterList) + +NS_IMETHODIMP nsMsgFilterList::CreateFilter(const nsAString& name, + class nsIMsgFilter** aFilter) { + NS_ENSURE_ARG_POINTER(aFilter); + + NS_ADDREF(*aFilter = new nsMsgFilter); + + (*aFilter)->SetFilterName(name); + (*aFilter)->SetFilterList(this); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterList::SetLoggingEnabled(bool enabled) { + if (!enabled) { + // Disabling logging has side effect of closing logfile (if open). + SetLogStream(nullptr); + } + m_loggingEnabled = enabled; + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterList::GetLoggingEnabled(bool* enabled) { + *enabled = m_loggingEnabled; + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterList::GetListId(nsACString& aListId) { + aListId.Assign(m_listId); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterList::GetFolder(nsIMsgFolder** aFolder) { + NS_ENSURE_ARG_POINTER(aFolder); + + NS_IF_ADDREF(*aFolder = m_folder); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterList::SetFolder(nsIMsgFolder* aFolder) { + m_folder = aFolder; + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterList::SaveToFile(nsIOutputStream* stream) { + if (!stream) return NS_ERROR_NULL_POINTER; + return SaveTextFilters(stream); +} + +#define LOG_HEADER \ + "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<style " \ + "type=\"text/css\">body{font-family:Consolas,\"Lucida " \ + "Console\",Monaco,\"Courier " \ + "New\",Courier,monospace;font-size:small}</style>\n</head>\n<body>\n" +#define LOG_HEADER_LEN (strlen(LOG_HEADER)) + +nsresult nsMsgFilterList::EnsureLogFile(nsIFile* file) { + bool exists; + nsresult rv = file->Exists(&exists); + if (NS_SUCCEEDED(rv) && !exists) { + rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 0666); + NS_ENSURE_SUCCESS(rv, rv); + } + + int64_t fileSize; + rv = file->GetFileSize(&fileSize); + NS_ENSURE_SUCCESS(rv, rv); + + // write the header at the start + if (fileSize == 0) { + nsCOMPtr<nsIOutputStream> outputStream; + rv = MsgGetFileStream(file, getter_AddRefs(outputStream)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t writeCount; + rv = outputStream->Write(LOG_HEADER, LOG_HEADER_LEN, &writeCount); + NS_ASSERTION(writeCount == LOG_HEADER_LEN, + "failed to write out log header"); + NS_ENSURE_SUCCESS(rv, rv); + outputStream->Close(); + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterList::ClearLog() { + bool loggingEnabled = m_loggingEnabled; + + // disable logging while clearing (and close logStream if open). + SetLoggingEnabled(false); + + nsCOMPtr<nsIFile> file; + if (NS_SUCCEEDED(GetLogFile(getter_AddRefs(file)))) { + file->Remove(false); + // Recreate the file, with just the html header. + EnsureLogFile(file); + } + + SetLoggingEnabled(loggingEnabled); + return NS_OK; +} + +nsresult nsMsgFilterList::GetLogFile(nsIFile** aFile) { + NS_ENSURE_ARG_POINTER(aFile); + + // XXX todo + // the path to the log file won't change + // should we cache it? + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = m_folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString type; + rv = server->GetType(type); + NS_ENSURE_SUCCESS(rv, rv); + + bool isServer = false; + rv = m_folder->GetIsServer(&isServer); + NS_ENSURE_SUCCESS(rv, rv); + + // for news folders (not servers), the filter file is + // mcom.test.dat + // where the summary file is + // mcom.test.msf + // since the log is an html file we make it + // mcom.test.htm + if (type.EqualsLiteral("nntp") && !isServer) { + nsCOMPtr<nsIFile> thisFolder; + rv = m_folder->GetFilePath(getter_AddRefs(thisFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> filterLogFile = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = filterLogFile->InitWithFile(thisFolder); + NS_ENSURE_SUCCESS(rv, rv); + + // NOTE: + // we don't we need to call NS_MsgHashIfNecessary() + // it's already been hashed, if necessary + nsAutoString filterLogName; + rv = filterLogFile->GetLeafName(filterLogName); + NS_ENSURE_SUCCESS(rv, rv); + + filterLogName.AppendLiteral(u".htm"); + + rv = filterLogFile->SetLeafName(filterLogName); + NS_ENSURE_SUCCESS(rv, rv); + + filterLogFile.forget(aFile); + } else { + rv = server->GetLocalPath(aFile); + NS_ENSURE_SUCCESS(rv, rv); + + rv = (*aFile)->AppendNative("filterlog.html"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + return EnsureLogFile(*aFile); +} + +NS_IMETHODIMP +nsMsgFilterList::GetLogURL(nsACString& aLogURL) { + nsCOMPtr<nsIFile> file; + nsresult rv = GetLogFile(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NS_GetURLSpecFromFile(file, aLogURL); + NS_ENSURE_SUCCESS(rv, rv); + + return !aLogURL.IsEmpty() ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP +nsMsgFilterList::SetLogStream(nsIOutputStream* aLogStream) { + // if there is a log stream already, close it + if (m_logStream) { + m_logStream->Close(); // will flush + } + + m_logStream = aLogStream; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilterList::GetLogStream(nsIOutputStream** aLogStream) { + NS_ENSURE_ARG_POINTER(aLogStream); + + if (!m_logStream && m_loggingEnabled) { + nsCOMPtr<nsIFile> logFile; + nsresult rv = GetLogFile(getter_AddRefs(logFile)); + if (NS_SUCCEEDED(rv)) { + // Make sure it exists and has it's initial header. + rv = EnsureLogFile(logFile); + if (NS_SUCCEEDED(rv)) { + // append to the end of the log file + rv = MsgNewBufferedFileOutputStream( + getter_AddRefs(m_logStream), logFile, + PR_CREATE_FILE | PR_WRONLY | PR_APPEND, 0666); + } + } + if (NS_FAILED(rv)) { + m_logStream = nullptr; + } + } + + // Always returns NS_OK. The stream can be null. + NS_IF_ADDREF(*aLogStream = m_logStream); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilterList::ApplyFiltersToHdr(nsMsgFilterTypeType filterType, + nsIMsgDBHdr* msgHdr, nsIMsgFolder* folder, + nsIMsgDatabase* db, + const nsACString& headers, + nsIMsgFilterHitNotify* listener, + nsIMsgWindow* msgWindow) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Auto) nsMsgFilterList::ApplyFiltersToHdr")); + if (!msgHdr) { + // Sometimes we get here with no header, so let's not crash on that + // later on. + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Auto) Called with NULL message header, nothing to do")); + return NS_ERROR_NULL_POINTER; + } + + nsCOMPtr<nsIMsgFilter> filter; + uint32_t filterCount = 0; + nsresult rv = GetFilterCount(&filterCount); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<nsMsgSearchScopeTerm> scope = + new nsMsgSearchScopeTerm(nullptr, nsMsgSearchScope::offlineMail, folder); + + nsString folderName; + folder->GetName(folderName); + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + nsCString typeName; + nsCOMPtr<nsIMsgFilterService> filterService = + do_GetService("@mozilla.org/messenger/services/filters;1", &rv); + filterService->FilterTypeName(filterType, typeName); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Auto) Filter run initiated, trigger=%s (%i)", typeName.get(), + filterType)); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Auto) Running %" PRIu32 + " filters from %s on message with key %" PRIu32 " in folder '%s'", + filterCount, m_listId.get(), msgKeyToInt(msgKey), + NS_ConvertUTF16toUTF8(folderName).get())); + + for (uint32_t filterIndex = 0; filterIndex < filterCount; filterIndex++) { + if (NS_SUCCEEDED(GetFilterAt(filterIndex, getter_AddRefs(filter)))) { + bool isEnabled; + nsMsgFilterTypeType curFilterType; + + filter->GetEnabled(&isEnabled); + if (!isEnabled) { + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Auto) Skipping disabled filter at index %" PRIu32, + filterIndex)); + // clang-format on + continue; + } + + nsString filterName; + filter->GetFilterName(filterName); + filter->GetFilterType(&curFilterType); + if (curFilterType & filterType) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Auto) Running filter %" PRIu32, filterIndex)); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Auto) Filter name: %s", + NS_ConvertUTF16toUTF8(filterName).get())); + + nsresult matchTermStatus = NS_OK; + bool result = false; + + filter->SetScope(scope); + matchTermStatus = + filter->MatchHdr(msgHdr, folder, db, headers, &result); + filter->SetScope(nullptr); + if (NS_SUCCEEDED(matchTermStatus) && result && listener) { + nsCString msgId; + msgHdr->GetMessageId(getter_Copies(msgId)); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Auto) Filter matched message with key %" PRIu32, + msgKeyToInt(msgKey))); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Auto) Matched message ID: %s", msgId.get())); + + bool applyMore = true; + rv = listener->ApplyFilterHit(filter, msgWindow, &applyMore); + if (NS_FAILED(rv)) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, + ("(Auto) Applying filter actions failed")); + LogFilterMessage(u"Applying filter actions failed"_ns, filter); + } else { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Auto) Applying filter actions succeeded")); + } + if (NS_FAILED(rv) || !applyMore) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Auto) Stopping further filter execution" + " on this message")); + break; + } + } else { + if (NS_FAILED(matchTermStatus)) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, + ("(Auto) Filter evaluation failed")); + LogFilterMessage(u"Filter evaluation failed"_ns, filter); + } + if (!result) + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Auto) Filter didn't match")); + } + } else { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Auto) Skipping filter of non-matching type" + " at index %" PRIu32, + filterIndex)); + } + } + } + if (NS_FAILED(rv)) { + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, + ("(Auto) Filter run failed (%" PRIx32 ")", static_cast<uint32_t>(rv))); + // clang-format on + LogFilterMessage(u"Filter run failed"_ns, nullptr); + } + return rv; +} + +NS_IMETHODIMP +nsMsgFilterList::SetDefaultFile(nsIFile* aFile) { + m_defaultFile = aFile; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilterList::GetDefaultFile(nsIFile** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + NS_IF_ADDREF(*aResult = m_defaultFile); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilterList::SaveToDefaultFile() { + nsresult rv; + nsCOMPtr<nsIMsgFilterService> filterService = + do_GetService("@mozilla.org/messenger/services/filters;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return filterService->SaveFilterList(this, m_defaultFile); +} + +typedef struct { + nsMsgFilterFileAttribValue attrib; + const char* attribName; +} FilterFileAttribEntry; + +static FilterFileAttribEntry FilterFileAttribTable[] = { + {nsIMsgFilterList::attribNone, ""}, + {nsIMsgFilterList::attribVersion, "version"}, + {nsIMsgFilterList::attribLogging, "logging"}, + {nsIMsgFilterList::attribName, "name"}, + {nsIMsgFilterList::attribEnabled, "enabled"}, + {nsIMsgFilterList::attribDescription, "description"}, + {nsIMsgFilterList::attribType, "type"}, + {nsIMsgFilterList::attribScriptFile, "scriptName"}, + {nsIMsgFilterList::attribAction, "action"}, + {nsIMsgFilterList::attribActionValue, "actionValue"}, + {nsIMsgFilterList::attribCondition, "condition"}, + {nsIMsgFilterList::attribCustomId, "customId"}, +}; + +static const unsigned int sNumFilterFileAttribTable = + MOZ_ARRAY_LENGTH(FilterFileAttribTable); + +// If we want to buffer file IO, wrap it in here. +int nsMsgFilterList::ReadChar(nsIInputStream* aStream) { + char newChar; + uint32_t bytesRead; + uint64_t bytesAvailable; + nsresult rv = aStream->Available(&bytesAvailable); + if (NS_FAILED(rv) || bytesAvailable == 0) return EOF_CHAR; + + rv = aStream->Read(&newChar, 1, &bytesRead); + if (NS_FAILED(rv) || !bytesRead) return EOF_CHAR; + + if (m_startWritingToBuffer) m_unparsedFilterBuffer.Append(newChar); + return (unsigned char)newChar; // Make sure the char is unsigned. +} + +int nsMsgFilterList::SkipWhitespace(nsIInputStream* aStream) { + int ch; + do { + ch = ReadChar(aStream); + } while (!(ch & 0x80) && + isspace(ch)); // isspace can crash with non-ascii input + + return ch; +} + +bool nsMsgFilterList::StrToBool(nsCString& str) { + return str.EqualsLiteral("yes"); +} + +int nsMsgFilterList::LoadAttrib(nsMsgFilterFileAttribValue& attrib, + nsIInputStream* aStream) { + char attribStr[100]; + int curChar; + attrib = nsIMsgFilterList::attribNone; + + curChar = SkipWhitespace(aStream); + int i; + for (i = 0; i + 1 < (int)(sizeof(attribStr));) { + if (curChar == EOF_CHAR || (!(curChar & 0x80) && isspace(curChar)) || + curChar == '=') + break; + attribStr[i++] = curChar; + curChar = ReadChar(aStream); + } + attribStr[i] = '\0'; + for (unsigned int tableIndex = 0; tableIndex < sNumFilterFileAttribTable; + tableIndex++) { + if (!PL_strcasecmp(attribStr, + FilterFileAttribTable[tableIndex].attribName)) { + attrib = FilterFileAttribTable[tableIndex].attrib; + break; + } + } + return curChar; +} + +const char* nsMsgFilterList::GetStringForAttrib( + nsMsgFilterFileAttribValue attrib) { + for (unsigned int tableIndex = 0; tableIndex < sNumFilterFileAttribTable; + tableIndex++) { + if (attrib == FilterFileAttribTable[tableIndex].attrib) + return FilterFileAttribTable[tableIndex].attribName; + } + return nullptr; +} + +nsresult nsMsgFilterList::LoadValue(nsCString& value, nsIInputStream* aStream) { + nsAutoCString valueStr; + int curChar; + value = ""; + curChar = SkipWhitespace(aStream); + if (curChar != '"') { + NS_ASSERTION(false, "expecting quote as start of value"); + return NS_MSG_FILTER_PARSE_ERROR; + } + curChar = ReadChar(aStream); + do { + if (curChar == '\\') { + int nextChar = ReadChar(aStream); + if (nextChar == '"') + curChar = '"'; + else if (nextChar == '\\') // replace "\\" with "\" + { + valueStr += curChar; + curChar = ReadChar(aStream); + } else { + valueStr += curChar; + curChar = nextChar; + } + } else { + if (curChar == EOF_CHAR || curChar == '"' || curChar == '\n' || + curChar == '\r') { + value += valueStr; + break; + } + } + valueStr += curChar; + curChar = ReadChar(aStream); + } while (curChar != EOF_CHAR); + return NS_OK; +} + +nsresult nsMsgFilterList::LoadTextFilters( + already_AddRefed<nsIInputStream> aStream) { + nsresult err = NS_OK; + uint64_t bytesAvailable; + + nsCOMPtr<nsIInputStream> bufStream; + nsCOMPtr<nsIInputStream> stream = std::move(aStream); + err = NS_NewBufferedInputStream(getter_AddRefs(bufStream), stream.forget(), + FILE_IO_BUFFER_SIZE); + NS_ENSURE_SUCCESS(err, err); + + nsMsgFilterFileAttribValue attrib; + nsCOMPtr<nsIMsgRuleAction> currentFilterAction; + // We'd really like to move lot's of these into the objects that they refer + // to. + do { + nsAutoCString value; + nsresult intToStringResult; + + int curChar; + curChar = LoadAttrib(attrib, bufStream); + if (curChar == EOF_CHAR) // reached eof + break; + err = LoadValue(value, bufStream); + if (NS_FAILED(err)) break; + + switch (attrib) { + case nsIMsgFilterList::attribNone: + if (m_curFilter) m_curFilter->SetUnparseable(true); + break; + case nsIMsgFilterList::attribVersion: + m_fileVersion = value.ToInteger(&intToStringResult); + if (NS_FAILED(intToStringResult)) { + attrib = nsIMsgFilterList::attribNone; + NS_ASSERTION(false, "error parsing filter file version"); + } + break; + case nsIMsgFilterList::attribLogging: + m_loggingEnabled = StrToBool(value); + // We are going to buffer each filter as we read them. + // Make sure no garbage is there + m_unparsedFilterBuffer.Truncate(); + m_startWritingToBuffer = true; // filters begin now + break; + case nsIMsgFilterList::attribName: // every filter starts w/ a name + { + if (m_curFilter) { + int32_t nextFilterStartPos = m_unparsedFilterBuffer.RFind("name"); + + nsAutoCString nextFilterPart; + nextFilterPart = Substring(m_unparsedFilterBuffer, nextFilterStartPos, + m_unparsedFilterBuffer.Length()); + m_unparsedFilterBuffer.SetLength(nextFilterStartPos); + + bool unparseableFilter; + m_curFilter->GetUnparseable(&unparseableFilter); + if (unparseableFilter) { + m_curFilter->SetUnparsedBuffer(m_unparsedFilterBuffer); + m_curFilter->SetEnabled(false); // disable the filter because we + // don't know how to apply it + } + m_unparsedFilterBuffer = nextFilterPart; + } + nsMsgFilter* filter = new nsMsgFilter; + if (filter == nullptr) { + err = NS_ERROR_OUT_OF_MEMORY; + break; + } + filter->SetFilterList(static_cast<nsIMsgFilterList*>(this)); + nsAutoString unicodeStr; + if (m_fileVersion == k45Version) { + NS_CopyNativeToUnicode(value, unicodeStr); + filter->SetFilterName(unicodeStr); + } else { + CopyUTF8toUTF16(value, unicodeStr); + filter->SetFilterName(unicodeStr); + } + m_curFilter = filter; + m_filters.AppendElement(filter); + } break; + case nsIMsgFilterList::attribEnabled: + if (m_curFilter) m_curFilter->SetEnabled(StrToBool(value)); + break; + case nsIMsgFilterList::attribDescription: + if (m_curFilter) m_curFilter->SetFilterDesc(value); + break; + case nsIMsgFilterList::attribType: + if (m_curFilter) { + // Older versions of filters didn't have the ability to turn on/off + // the manual filter context, so default manual to be on in that case + int32_t filterType = value.ToInteger(&intToStringResult); + if (m_fileVersion < kManualContextVersion) + filterType |= nsMsgFilterType::Manual; + m_curFilter->SetType((nsMsgFilterTypeType)filterType); + } + break; + case nsIMsgFilterList::attribScriptFile: + if (m_curFilter) m_curFilter->SetFilterScript(&value); + break; + case nsIMsgFilterList::attribAction: + if (m_curFilter) { + nsMsgRuleActionType actionType = + nsMsgFilter::GetActionForFilingStr(value); + if (actionType == nsMsgFilterAction::None) + m_curFilter->SetUnparseable(true); + else { + err = + m_curFilter->CreateAction(getter_AddRefs(currentFilterAction)); + NS_ENSURE_SUCCESS(err, err); + currentFilterAction->SetType(actionType); + m_curFilter->AppendAction(currentFilterAction); + } + } + break; + case nsIMsgFilterList::attribActionValue: + if (m_curFilter && currentFilterAction) { + nsMsgRuleActionType type; + currentFilterAction->GetType(&type); + if (type == nsMsgFilterAction::MoveToFolder || + type == nsMsgFilterAction::CopyToFolder) + err = m_curFilter->ConvertMoveOrCopyToFolderValue( + currentFilterAction, value); + else if (type == nsMsgFilterAction::ChangePriority) { + nsMsgPriorityValue outPriority; + nsresult res = + NS_MsgGetPriorityFromString(value.get(), outPriority); + if (NS_SUCCEEDED(res)) + currentFilterAction->SetPriority(outPriority); + else + NS_ASSERTION(false, "invalid priority in filter file"); + } else if (type == nsMsgFilterAction::JunkScore) { + nsresult res; + int32_t junkScore = value.ToInteger(&res); + if (NS_SUCCEEDED(res)) currentFilterAction->SetJunkScore(junkScore); + } else if (type == nsMsgFilterAction::Forward || + type == nsMsgFilterAction::Reply || + type == nsMsgFilterAction::AddTag || + type == nsMsgFilterAction::Custom) { + currentFilterAction->SetStrValue(value); + } + } + break; + case nsIMsgFilterList::attribCondition: + if (m_curFilter) { + if (m_fileVersion == k45Version) { + nsAutoString unicodeStr; + NS_CopyNativeToUnicode(value, unicodeStr); + CopyUTF16toUTF8(unicodeStr, value); + } + err = ParseCondition(m_curFilter, value.get()); + if (err == NS_ERROR_INVALID_ARG) + err = m_curFilter->SetUnparseable(true); + NS_ENSURE_SUCCESS(err, err); + } + break; + case nsIMsgFilterList::attribCustomId: + if (m_curFilter && currentFilterAction) { + err = currentFilterAction->SetCustomId(value); + NS_ENSURE_SUCCESS(err, err); + } + break; + } + } while (NS_SUCCEEDED(bufStream->Available(&bytesAvailable))); + + if (m_curFilter) { + bool unparseableFilter; + m_curFilter->GetUnparseable(&unparseableFilter); + if (unparseableFilter) { + m_curFilter->SetUnparsedBuffer(m_unparsedFilterBuffer); + m_curFilter->SetEnabled( + false); // disable the filter because we don't know how to apply it + } + } + + return err; +} + +// parse condition like "(subject, contains, fred) AND (body, isn't, "foo)")" +// values with close parens will be quoted. +// what about values with close parens and quotes? e.g., (body, isn't, "foo")") +// I guess interior quotes will need to be escaped - ("foo\")") +// which will get written out as (\"foo\\")\") and read in as ("foo\")" +// ALL means match all messages. +NS_IMETHODIMP nsMsgFilterList::ParseCondition(nsIMsgFilter* aFilter, + const char* aCondition) { + NS_ENSURE_ARG_POINTER(aFilter); + + bool done = false; + nsresult err = NS_OK; + const char* curPtr = aCondition; + if (!strcmp(aCondition, "ALL")) { + RefPtr<nsMsgSearchTerm> newTerm = new nsMsgSearchTerm; + newTerm->m_matchAll = true; + aFilter->AppendTerm(newTerm); + return NS_OK; + } + + while (!done) { + // insert code to save the boolean operator if there is one for this search + // term.... + const char* openParen = PL_strchr(curPtr, '('); + const char* orTermPos = PL_strchr( + curPtr, 'O'); // determine if an "OR" appears b4 the openParen... + bool ANDTerm = true; + if (orTermPos && + orTermPos < openParen) // make sure OR term falls before the '(' + ANDTerm = false; + + char* termDup = nullptr; + if (openParen) { + bool foundEndTerm = false; + bool inQuote = false; + for (curPtr = openParen + 1; *curPtr; curPtr++) { + if (*curPtr == '\\' && *(curPtr + 1) == '"') + curPtr++; + else if (*curPtr == ')' && !inQuote) { + foundEndTerm = true; + break; + } else if (*curPtr == '"') + inQuote = !inQuote; + } + if (foundEndTerm) { + int termLen = curPtr - openParen - 1; + termDup = (char*)PR_Malloc(termLen + 1); + if (termDup) { + PL_strncpy(termDup, openParen + 1, termLen + 1); + termDup[termLen] = '\0'; + } else { + err = NS_ERROR_OUT_OF_MEMORY; + break; + } + } + } else + break; + if (termDup) { + RefPtr<nsMsgSearchTerm> newTerm = new nsMsgSearchTerm; + // Invert nsMsgSearchTerm::EscapeQuotesInStr() + for (char *to = termDup, *from = termDup;;) { + if (*from == '\\' && from[1] == '"') from++; + if (!(*to++ = *from++)) break; + } + newTerm->m_booleanOp = (ANDTerm) ? nsMsgSearchBooleanOp::BooleanAND + : nsMsgSearchBooleanOp::BooleanOR; + + err = newTerm->DeStreamNew(termDup, PL_strlen(termDup)); + NS_ENSURE_SUCCESS(err, err); + aFilter->AppendTerm(newTerm); + PR_FREEIF(termDup); + } else + break; + } + return err; +} + +nsresult nsMsgFilterList::WriteIntAttr(nsMsgFilterFileAttribValue attrib, + int value, nsIOutputStream* aStream) { + nsresult rv = NS_OK; + const char* attribStr = GetStringForAttrib(attrib); + if (attribStr) { + uint32_t bytesWritten; + nsAutoCString writeStr(attribStr); + writeStr.AppendLiteral("=\""); + writeStr.AppendInt(value); + writeStr.AppendLiteral("\"" MSG_LINEBREAK); + rv = aStream->Write(writeStr.get(), writeStr.Length(), &bytesWritten); + } + return rv; +} + +NS_IMETHODIMP +nsMsgFilterList::WriteStrAttr(nsMsgFilterFileAttribValue attrib, + const char* aStr, nsIOutputStream* aStream) { + nsresult rv = NS_OK; + if (aStr && *aStr && + aStream) // only proceed if we actually have a string to write out. + { + char* escapedStr = nullptr; + if (PL_strchr(aStr, '"')) + escapedStr = nsMsgSearchTerm::EscapeQuotesInStr(aStr); + + const char* attribStr = GetStringForAttrib(attrib); + if (attribStr) { + uint32_t bytesWritten; + nsAutoCString writeStr(attribStr); + writeStr.AppendLiteral("=\""); + writeStr.Append((escapedStr) ? escapedStr : aStr); + writeStr.AppendLiteral("\"" MSG_LINEBREAK); + rv = aStream->Write(writeStr.get(), writeStr.Length(), &bytesWritten); + } + PR_Free(escapedStr); + } + return rv; +} + +nsresult nsMsgFilterList::WriteBoolAttr(nsMsgFilterFileAttribValue attrib, + bool boolVal, + nsIOutputStream* aStream) { + return WriteStrAttr(attrib, (boolVal) ? "yes" : "no", aStream); +} + +nsresult nsMsgFilterList::WriteWstrAttr(nsMsgFilterFileAttribValue attrib, + const char16_t* aFilterName, + nsIOutputStream* aStream) { + WriteStrAttr(attrib, NS_ConvertUTF16toUTF8(aFilterName).get(), aStream); + return NS_OK; +} + +nsresult nsMsgFilterList::SaveTextFilters(nsIOutputStream* aStream) { + uint32_t filterCount = 0; + nsresult err = GetFilterCount(&filterCount); + NS_ENSURE_SUCCESS(err, err); + + err = WriteIntAttr(nsIMsgFilterList::attribVersion, kFileVersion, aStream); + NS_ENSURE_SUCCESS(err, err); + err = + WriteBoolAttr(nsIMsgFilterList::attribLogging, m_loggingEnabled, aStream); + NS_ENSURE_SUCCESS(err, err); + for (uint32_t i = 0; i < filterCount; i++) { + nsCOMPtr<nsIMsgFilter> filter; + if (NS_SUCCEEDED(GetFilterAt(i, getter_AddRefs(filter))) && filter) { + filter->SetFilterList(this); + + // if the filter is temporary, don't write it to disk + bool isTemporary; + err = filter->GetTemporary(&isTemporary); + if (NS_SUCCEEDED(err) && !isTemporary) { + err = filter->SaveToTextFile(aStream); + if (NS_FAILED(err)) break; + } + } else + break; + } + if (NS_SUCCEEDED(err)) m_arbitraryHeaders.Truncate(); + return err; +} + +nsMsgFilterList::~nsMsgFilterList() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Closing filter list %s", m_listId.get())); +} + +nsresult nsMsgFilterList::Close() { return NS_ERROR_NOT_IMPLEMENTED; } + +nsresult nsMsgFilterList::GetFilterCount(uint32_t* pCount) { + NS_ENSURE_ARG_POINTER(pCount); + + *pCount = m_filters.Length(); + return NS_OK; +} + +nsresult nsMsgFilterList::GetFilterAt(uint32_t filterIndex, + nsIMsgFilter** filter) { + NS_ENSURE_ARG_POINTER(filter); + + uint32_t filterCount = 0; + GetFilterCount(&filterCount); + NS_ENSURE_ARG(filterIndex < filterCount); + + NS_IF_ADDREF(*filter = m_filters[filterIndex]); + return NS_OK; +} + +nsresult nsMsgFilterList::GetFilterNamed(const nsAString& aName, + nsIMsgFilter** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + uint32_t count = 0; + nsresult rv = GetFilterCount(&count); + NS_ENSURE_SUCCESS(rv, rv); + + *aResult = nullptr; + for (uint32_t i = 0; i < count; i++) { + nsCOMPtr<nsIMsgFilter> filter; + rv = GetFilterAt(i, getter_AddRefs(filter)); + if (NS_FAILED(rv)) continue; + + nsString filterName; + filter->GetFilterName(filterName); + if (filterName.Equals(aName)) { + filter.forget(aResult); + break; + } + } + + return NS_OK; +} + +nsresult nsMsgFilterList::SetFilterAt(uint32_t filterIndex, + nsIMsgFilter* filter) { + m_filters[filterIndex] = filter; + return NS_OK; +} + +nsresult nsMsgFilterList::RemoveFilterAt(uint32_t filterIndex) { + m_filters.RemoveElementAt(filterIndex); + return NS_OK; +} + +nsresult nsMsgFilterList::RemoveFilter(nsIMsgFilter* aFilter) { + m_filters.RemoveElement(aFilter); + return NS_OK; +} + +nsresult nsMsgFilterList::InsertFilterAt(uint32_t filterIndex, + nsIMsgFilter* aFilter) { + if (!m_temporaryList) aFilter->SetFilterList(this); + m_filters.InsertElementAt(filterIndex, aFilter); + + return NS_OK; +} + +// Attempt to move the filter at index filterIndex in the specified direction. +// If motion not possible in that direction, we still return success. +// We could return an error if the FE's want to beep or something. +nsresult nsMsgFilterList::MoveFilterAt(uint32_t filterIndex, + nsMsgFilterMotionValue motion) { + NS_ENSURE_ARG((motion == nsMsgFilterMotion::up) || + (motion == nsMsgFilterMotion::down)); + + uint32_t filterCount = 0; + nsresult rv = GetFilterCount(&filterCount); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ENSURE_ARG(filterIndex < filterCount); + + uint32_t newIndex = filterIndex; + + if (motion == nsMsgFilterMotion::up) { + // are we already at the top? + if (filterIndex == 0) return NS_OK; + + newIndex = filterIndex - 1; + } else if (motion == nsMsgFilterMotion::down) { + // are we already at the bottom? + if (filterIndex == filterCount - 1) return NS_OK; + + newIndex = filterIndex + 1; + } + + nsCOMPtr<nsIMsgFilter> tempFilter1; + rv = GetFilterAt(newIndex, getter_AddRefs(tempFilter1)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFilter> tempFilter2; + rv = GetFilterAt(filterIndex, getter_AddRefs(tempFilter2)); + NS_ENSURE_SUCCESS(rv, rv); + + SetFilterAt(newIndex, tempFilter2); + SetFilterAt(filterIndex, tempFilter1); + + return NS_OK; +} + +nsresult nsMsgFilterList::MoveFilter(nsIMsgFilter* aFilter, + nsMsgFilterMotionValue motion) { + size_t filterIndex = m_filters.IndexOf(aFilter, 0); + NS_ENSURE_ARG(filterIndex != m_filters.NoIndex); + + return MoveFilterAt(filterIndex, motion); +} + +nsresult nsMsgFilterList::GetVersion(int16_t* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = m_fileVersion; + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterList::MatchOrChangeFilterTarget( + const nsACString& oldFolderUri, const nsACString& newFolderUri, + bool caseInsensitive, bool* found) { + NS_ENSURE_ARG_POINTER(found); + + uint32_t numFilters = 0; + nsresult rv = GetFilterCount(&numFilters); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFilter> filter; + nsCString folderUri; + *found = false; + for (uint32_t index = 0; index < numFilters; index++) { + rv = GetFilterAt(index, getter_AddRefs(filter)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t numActions; + rv = filter->GetActionCount(&numActions); + NS_ENSURE_SUCCESS(rv, rv); + + for (uint32_t actionIndex = 0; actionIndex < numActions; actionIndex++) { + nsCOMPtr<nsIMsgRuleAction> filterAction; + rv = filter->GetActionAt(actionIndex, getter_AddRefs(filterAction)); + if (NS_FAILED(rv) || !filterAction) continue; + + nsMsgRuleActionType actionType; + if (NS_FAILED(filterAction->GetType(&actionType))) continue; + + if (actionType == nsMsgFilterAction::MoveToFolder || + actionType == nsMsgFilterAction::CopyToFolder) { + rv = filterAction->GetTargetFolderUri(folderUri); + if (NS_SUCCEEDED(rv) && !folderUri.IsEmpty()) { + bool matchFound = false; + if (caseInsensitive) { + if (folderUri.Equals(oldFolderUri, + nsCaseInsensitiveCStringComparator)) // local + matchFound = true; + } else { + if (folderUri.Equals(oldFolderUri)) // imap + matchFound = true; + } + if (matchFound) { + *found = true; + // if we just want to match the uri's, newFolderUri will be null + if (!newFolderUri.IsEmpty()) { + rv = filterAction->SetTargetFolderUri(newFolderUri); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + } + } + } + return rv; +} + +// this would only return true if any filter was on "any header", which we +// don't support in 6.x +NS_IMETHODIMP nsMsgFilterList::GetShouldDownloadAllHeaders(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + *aResult = false; + return NS_OK; +} + +// leaves m_arbitraryHeaders filed in with the arbitrary headers. +nsresult nsMsgFilterList::ComputeArbitraryHeaders() { + NS_ENSURE_TRUE(m_arbitraryHeaders.IsEmpty(), NS_OK); + + uint32_t numFilters = 0; + nsresult rv = GetFilterCount(&numFilters); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFilter> filter; + nsMsgSearchAttribValue attrib; + nsCString arbitraryHeader; + for (uint32_t index = 0; index < numFilters; index++) { + rv = GetFilterAt(index, getter_AddRefs(filter)); + if (!(NS_SUCCEEDED(rv) && filter)) continue; + + nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms; + filter->GetSearchTerms(searchTerms); + for (uint32_t i = 0; i < searchTerms.Length(); i++) { + filter->GetTerm(i, &attrib, nullptr, nullptr, nullptr, arbitraryHeader); + if (!arbitraryHeader.IsEmpty()) { + if (m_arbitraryHeaders.IsEmpty()) + m_arbitraryHeaders.Assign(arbitraryHeader); + else if (!FindInReadable(arbitraryHeader, m_arbitraryHeaders, + nsCaseInsensitiveCStringComparator)) { + m_arbitraryHeaders.Append(' '); + m_arbitraryHeaders.Append(arbitraryHeader); + } + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterList::GetArbitraryHeaders(nsACString& aResult) { + ComputeArbitraryHeaders(); + aResult = m_arbitraryHeaders; + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterList::FlushLogIfNecessary() { + // only flush the log if we are logging + if (m_loggingEnabled && m_logStream) { + nsresult rv = m_logStream->Flush(); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +#define LOG_ENTRY_START_TAG "<p>\n" +#define LOG_ENTRY_START_TAG_LEN (strlen(LOG_ENTRY_START_TAG)) +#define LOG_ENTRY_END_TAG "</p>\n" +#define LOG_ENTRY_END_TAG_LEN (strlen(LOG_ENTRY_END_TAG)) + +NS_IMETHODIMP nsMsgFilterList::LogFilterMessage(const nsAString& message, + nsIMsgFilter* filter) { + if (!m_loggingEnabled) { + return NS_OK; + } + nsCOMPtr<nsIOutputStream> logStream; + GetLogStream(getter_AddRefs(logStream)); + if (!logStream) { + // Logging is on, but we failed to access the filter logfile. + // For completeness, we'll return an error, but we don't expect anyone + // to ever check it - logging failures shouldn't stop anything else. + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv = bundleService->CreateBundle( + "chrome://messenger/locale/filter.properties", getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString tempMessage(message); + + if (filter) { + // If a filter was passed, prepend its name in the log message. + nsString filterName; + filter->GetFilterName(filterName); + + AutoTArray<nsString, 2> logFormatStrings = {filterName, tempMessage}; + nsString statusLogMessage; + rv = bundle->FormatStringFromName("filterMessage", logFormatStrings, + statusLogMessage); + if (NS_SUCCEEDED(rv)) tempMessage.Assign(statusLogMessage); + } + + // Prepare timestamp + PRExplodedTime exploded; + nsString dateValue; + PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded); + mozilla::intl::DateTimeFormat::StyleBag style; + style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short); + style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long); + mozilla::intl::AppDateTimeFormat::Format(style, &exploded, dateValue); + + // HTML-escape the log for security reasons. + // We don't want someone to send us a message with a subject with + // HTML tags, especially <script>. + nsCString escapedBuffer; + nsAppendEscapedHTML(NS_ConvertUTF16toUTF8(tempMessage), escapedBuffer); + + // Print timestamp and the message. + AutoTArray<nsString, 2> logFormatStrings = {dateValue}; + CopyUTF8toUTF16(escapedBuffer, *logFormatStrings.AppendElement()); + nsString filterLogMessage; + rv = bundle->FormatStringFromName("filterLogLine", logFormatStrings, + filterLogMessage); + NS_ENSURE_SUCCESS(rv, rv); + + // Write message into log stream. + uint32_t writeCount; + rv = logStream->Write(LOG_ENTRY_START_TAG, LOG_ENTRY_START_TAG_LEN, + &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(writeCount == LOG_ENTRY_START_TAG_LEN, + "failed to write out start log tag"); + + NS_ConvertUTF16toUTF8 buffer(filterLogMessage); + uint32_t escapedBufferLen = buffer.Length(); + rv = logStream->Write(buffer.get(), escapedBufferLen, &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(writeCount == escapedBufferLen, "failed to write out log hit"); + + rv = logStream->Write(LOG_ENTRY_END_TAG, LOG_ENTRY_END_TAG_LEN, &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(writeCount == LOG_ENTRY_END_TAG_LEN, + "failed to write out end log tag"); + return NS_OK; +} +// ------------ End FilterList methods ------------------ diff --git a/comm/mailnews/search/src/nsMsgFilterList.h b/comm/mailnews/search/src/nsMsgFilterList.h new file mode 100644 index 0000000000..536156854e --- /dev/null +++ b/comm/mailnews/search/src/nsMsgFilterList.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgFilterList_H_ +#define _nsMsgFilterList_H_ + +#include "nscore.h" +#include "nsIMsgFolder.h" +#include "nsIMsgFilterList.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "nsIFile.h" +#include "nsIOutputStream.h" + +const int16_t kFileVersion = 9; +const int16_t kManualContextVersion = 9; +const int16_t k60Beta1Version = 7; +const int16_t k45Version = 6; + +//////////////////////////////////////////////////////////////////////////////////////// +// The Msg Filter List is an interface designed to make accessing filter lists +// easier. Clients typically open a filter list and either enumerate the +// filters, or add new filters, or change the order around... +// +//////////////////////////////////////////////////////////////////////////////////////// + +class nsIMsgFilter; +class nsMsgFilter; + +class nsMsgFilterList : public nsIMsgFilterList { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGFILTERLIST + + nsMsgFilterList(); + + nsresult Close(); + nsresult LoadTextFilters(already_AddRefed<nsIInputStream> aStream); + + bool m_temporaryList; + + protected: + virtual ~nsMsgFilterList(); + + nsresult ComputeArbitraryHeaders(); + nsresult SaveTextFilters(nsIOutputStream* aStream); + // file streaming methods + int ReadChar(nsIInputStream* aStream); + int SkipWhitespace(nsIInputStream* aStream); + bool StrToBool(nsCString& str); + int LoadAttrib(nsMsgFilterFileAttribValue& attrib, nsIInputStream* aStream); + const char* GetStringForAttrib(nsMsgFilterFileAttribValue attrib); + nsresult LoadValue(nsCString& value, nsIInputStream* aStream); + int16_t m_fileVersion; + bool m_loggingEnabled; + bool m_startWritingToBuffer; // tells us when to start writing one whole + // filter to m_unparsedBuffer + nsCOMPtr<nsIMsgFolder> m_folder; + nsMsgFilter* m_curFilter; // filter we're filing in or out(?) + nsCString m_listId; + nsTArray<nsCOMPtr<nsIMsgFilter> > m_filters; + nsCString m_arbitraryHeaders; + nsCOMPtr<nsIFile> m_defaultFile; + nsCString m_unparsedFilterBuffer; // holds one entire filter unparsed + + private: + nsresult GetLogFile(nsIFile** aFile); + nsresult EnsureLogFile(nsIFile* file); + nsCOMPtr<nsIOutputStream> m_logStream; +}; + +#endif diff --git a/comm/mailnews/search/src/nsMsgFilterService.cpp b/comm/mailnews/search/src/nsMsgFilterService.cpp new file mode 100644 index 0000000000..1adfe7cee9 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgFilterService.cpp @@ -0,0 +1,1374 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// this file implements the nsMsgFilterService interface + +#include "msgCore.h" +#include "nsMsgFilterService.h" +#include "nsMsgFilterList.h" +#include "nsMsgSearchScopeTerm.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIPrompt.h" +#include "nsIDocShell.h" +#include "nsIStringBundle.h" +#include "nsIMsgSearchNotify.h" +#include "nsIUrlListener.h" +#include "nsIMsgCopyServiceListener.h" +#include "nsIMsgLocalMailFolder.h" +#include "nsIMsgDatabase.h" +#include "nsIMsgHdr.h" +#include "nsIMsgCopyService.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsISafeOutputStream.h" +#include "nsIMsgComposeService.h" +#include "nsNetUtil.h" +#include "nsMsgUtils.h" +#include "nsIMsgMailSession.h" +#include "nsIFile.h" +#include "nsIMsgFilterCustomAction.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgWindow.h" +#include "nsIMsgSearchCustomTerm.h" +#include "nsIMsgSearchTerm.h" +#include "nsIMsgThread.h" +#include "nsIMsgFilter.h" +#include "nsIMsgOperationListener.h" +#include "mozilla/Components.h" +#include "mozilla/Logging.h" + +using namespace mozilla; + +LazyLogModule FILTERLOGMODULE("Filters"); + +#define BREAK_IF_FAILURE(_rv, _text) \ + if (NS_FAILED(_rv)) { \ + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, \ + ("(Post) Filter error: %s", _text)); \ + m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \ + m_curFilter); \ + NS_WARNING(_text); \ + mFinalResult = _rv; \ + break; \ + } + +#define CONTINUE_IF_FAILURE(_rv, _text) \ + if (NS_FAILED(_rv)) { \ + MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning, \ + ("(Post) Filter problem: %s", _text)); \ + m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \ + m_curFilter); \ + NS_WARNING(_text); \ + mFinalResult = _rv; \ + if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); \ + continue; \ + } + +#define BREAK_IF_FALSE(_assertTrue, _text) \ + if (MOZ_UNLIKELY(!(_assertTrue))) { \ + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, \ + ("(Post) Filter error: %s", _text)); \ + m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \ + m_curFilter); \ + NS_WARNING(_text); \ + mFinalResult = NS_ERROR_FAILURE; \ + break; \ + } + +#define CONTINUE_IF_FALSE(_assertTrue, _text) \ + if (MOZ_UNLIKELY(!(_assertTrue))) { \ + MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning, \ + ("(Post) Filter problem: %s", _text)); \ + m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \ + m_curFilter); \ + NS_WARNING(_text); \ + mFinalResult = NS_ERROR_FAILURE; \ + if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); \ + continue; \ + } + +#define BREAK_ACTION(_text) \ + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, \ + ("(Post) Filter Error: %s", _text)); \ + if (loggingEnabled) \ + m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \ + m_curFilter); \ + NS_WARNING(_text); \ + if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); \ + break; + +#define BREAK_ACTION_IF_FALSE(_assertTrue, _text) \ + if (MOZ_UNLIKELY(!(_assertTrue))) { \ + finalResult = NS_ERROR_FAILURE; \ + BREAK_ACTION(_text); \ + } + +#define BREAK_ACTION_IF_FAILURE(_rv, _text) \ + if (NS_FAILED(_rv)) { \ + finalResult = _rv; \ + BREAK_ACTION(_text); \ + } + +NS_IMPL_ISUPPORTS(nsMsgFilterService, nsIMsgFilterService) + +nsMsgFilterService::nsMsgFilterService() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, ("nsMsgFilterService")); +} + +nsMsgFilterService::~nsMsgFilterService() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, ("~nsMsgFilterService")); +} + +NS_IMETHODIMP nsMsgFilterService::OpenFilterList( + nsIFile* aFilterFile, nsIMsgFolder* rootFolder, nsIMsgWindow* aMsgWindow, + nsIMsgFilterList** resultFilterList) { + NS_ENSURE_ARG_POINTER(aFilterFile); + NS_ENSURE_ARG_POINTER(resultFilterList); + + nsresult rv; + if (rootFolder) { + nsCOMPtr<nsIMsgIncomingServer> server; + rv = rootFolder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + nsString serverName; + server->GetPrettyName(serverName); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Reading filter list for account '%s'", + NS_ConvertUTF16toUTF8(serverName).get())); + } + + nsString fileName; + (void)aFilterFile->GetPath(fileName); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("Reading filter list from file '%s'", + NS_ConvertUTF16toUTF8(fileName).get())); + + bool exists = false; + rv = aFilterFile->Exists(&exists); + if (NS_FAILED(rv) || !exists) { + rv = aFilterFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIInputStream> fileStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(fileStream), aFilterFile); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(fileStream, NS_ERROR_OUT_OF_MEMORY); + + RefPtr<nsMsgFilterList> filterList = new nsMsgFilterList(); + filterList->SetFolder(rootFolder); + + // temporarily tell the filter where its file path is + filterList->SetDefaultFile(aFilterFile); + + int64_t size = 0; + rv = aFilterFile->GetFileSize(&size); + if (NS_SUCCEEDED(rv) && size > 0) + rv = filterList->LoadTextFilters(fileStream.forget()); + if (NS_SUCCEEDED(rv)) { + int16_t version; + filterList->GetVersion(&version); + if (version != kFileVersion) SaveFilterList(filterList, aFilterFile); + } else { + if (rv == NS_MSG_FILTER_PARSE_ERROR && aMsgWindow) { + rv = BackUpFilterFile(aFilterFile, aMsgWindow); + NS_ENSURE_SUCCESS(rv, rv); + rv = aFilterFile->SetFileSize(0); + NS_ENSURE_SUCCESS(rv, rv); + return OpenFilterList(aFilterFile, rootFolder, aMsgWindow, + resultFilterList); + } else if (rv == NS_MSG_CUSTOM_HEADERS_OVERFLOW && aMsgWindow) + ThrowAlertMsg("filterCustomHeaderOverflow", aMsgWindow); + else if (rv == NS_MSG_INVALID_CUSTOM_HEADER && aMsgWindow) + ThrowAlertMsg("invalidCustomHeader", aMsgWindow); + } + + nsCString listId; + filterList->GetListId(listId); + uint32_t filterCount = 0; + (void)filterList->GetFilterCount(&filterCount); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Read %" PRIu32 " filters", filterCount)); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Filter list stored as %s", listId.get())); + + filterList.forget(resultFilterList); + return rv; +} + +NS_IMETHODIMP nsMsgFilterService::CloseFilterList( + nsIMsgFilterList* filterList) { + // NS_ASSERTION(false,"CloseFilterList doesn't do anything yet"); + return NS_OK; +} + +/* save without deleting */ +NS_IMETHODIMP nsMsgFilterService::SaveFilterList(nsIMsgFilterList* filterList, + nsIFile* filterFile) { + NS_ENSURE_ARG_POINTER(filterFile); + NS_ENSURE_ARG_POINTER(filterList); + + nsCString listId; + filterList->GetListId(listId); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Saving filter list %s", listId.get())); + + nsCOMPtr<nsIOutputStream> strm; + nsresult rv = MsgNewSafeBufferedFileOutputStream(getter_AddRefs(strm), + filterFile, -1, 0600); + NS_ENSURE_SUCCESS(rv, rv); + + rv = filterList->SaveToFile(strm); + + nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(strm); + NS_ASSERTION(safeStream, "expected a safe output stream!"); + if (safeStream) { + rv = safeStream->Finish(); + if (NS_FAILED(rv)) { + NS_WARNING("failed to save filter file! possible data loss"); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, ("Save of list failed")); + } + } + return rv; +} + +NS_IMETHODIMP nsMsgFilterService::CancelFilterList( + nsIMsgFilterList* filterList) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsMsgFilterService::BackUpFilterFile(nsIFile* aFilterFile, + nsIMsgWindow* aMsgWindow) { + AlertBackingUpFilterFile(aMsgWindow); + + nsCOMPtr<nsIFile> localParentDir; + nsresult rv = aFilterFile->GetParent(getter_AddRefs(localParentDir)); + NS_ENSURE_SUCCESS(rv, rv); + + // if back-up file exists delete the back up file otherwise copy fails. + nsCOMPtr<nsIFile> backupFile; + rv = localParentDir->Clone(getter_AddRefs(backupFile)); + NS_ENSURE_SUCCESS(rv, rv); + backupFile->AppendNative("rulesbackup.dat"_ns); + bool exists; + backupFile->Exists(&exists); + if (exists) backupFile->Remove(false); + + return aFilterFile->CopyToNative(localParentDir, "rulesbackup.dat"_ns); +} + +nsresult nsMsgFilterService::AlertBackingUpFilterFile( + nsIMsgWindow* aMsgWindow) { + return ThrowAlertMsg("filterListBackUpMsg", aMsgWindow); +} + +// Do not use this routine if you have to call it very often because it creates +// a new bundle each time. +nsresult nsMsgFilterService::GetStringFromBundle(const char* aMsgName, + nsAString& aResult) { + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv = GetFilterStringBundle(getter_AddRefs(bundle)); + if (NS_SUCCEEDED(rv) && bundle) + rv = bundle->GetStringFromName(aMsgName, aResult); + return rv; +} + +nsresult nsMsgFilterService::GetFilterStringBundle(nsIStringBundle** aBundle) { + NS_ENSURE_ARG_POINTER(aBundle); + + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsIStringBundle> bundle; + if (bundleService) + bundleService->CreateBundle("chrome://messenger/locale/filter.properties", + getter_AddRefs(bundle)); + bundle.forget(aBundle); + return NS_OK; +} + +nsresult nsMsgFilterService::ThrowAlertMsg(const char* aMsgName, + nsIMsgWindow* aMsgWindow) { + nsString alertString; + nsresult rv = GetStringFromBundle(aMsgName, alertString); + nsCOMPtr<nsIMsgWindow> msgWindow = aMsgWindow; + if (!msgWindow) { + nsCOMPtr<nsIMsgMailSession> mailSession( + do_GetService("@mozilla.org/messenger/services/session;1", &rv)); + if (NS_SUCCEEDED(rv)) + rv = mailSession->GetTopmostMsgWindow(getter_AddRefs(msgWindow)); + } + + if (NS_SUCCEEDED(rv) && !alertString.IsEmpty() && msgWindow) { + nsCOMPtr<nsIDocShell> docShell; + msgWindow->GetRootDocShell(getter_AddRefs(docShell)); + if (docShell) { + nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell)); + if (dialog && !alertString.IsEmpty()) + dialog->Alert(nullptr, alertString.get()); + } + } + return rv; +} + +// this class is used to run filters after the fact, i.e., after new mail has +// been downloaded from the server. It can do the following: +// 1. Apply a single imap or pop3 filter on a single folder. +// 2. Apply multiple filters on a single imap or pop3 folder. +// 3. Apply a single filter on multiple imap or pop3 folders in the same +// account. +// 4. Apply multiple filters on multiple imap or pop3 folders in the same +// account. +// This will be called from the front end js code in the case of the +// apply filters to folder menu code, and from the filter dialog js code with +// the run filter now command. + +// this class holds the list of filters and folders, and applies them in turn, +// first iterating over all the filters on one folder, and then advancing to the +// next folder and repeating. For each filter,we take the filter criteria and +// create a search term list. Then, we execute the search. We are a search +// listener so that we can build up the list of search hits. Then, when the +// search is done, we will apply the filter action(s) en-masse, so, for example, +// if the action is a move, we calls one method to move all the messages to the +// destination folder. Or, mark all the messages read. In the case of imap +// operations, or imap/local moves, the action will be asynchronous, so we'll +// need to be a url listener as well, and kick off the next filter when the +// action completes. +class nsMsgFilterAfterTheFact : public nsIUrlListener, + public nsIMsgSearchNotify, + public nsIMsgCopyServiceListener { + public: + nsMsgFilterAfterTheFact(nsIMsgWindow* aMsgWindow, + nsIMsgFilterList* aFilterList, + const nsTArray<RefPtr<nsIMsgFolder>>& aFolderList, + nsIMsgOperationListener* aCallback); + NS_DECL_ISUPPORTS + NS_DECL_NSIURLLISTENER + NS_DECL_NSIMSGSEARCHNOTIFY + NS_DECL_NSIMSGCOPYSERVICELISTENER + + nsresult AdvanceToNextFolder(); // kicks off the process + protected: + virtual ~nsMsgFilterAfterTheFact(); + virtual nsresult RunNextFilter(); + /** + * apply filter actions to current search hits + */ + nsresult ApplyFilter(); + nsresult OnEndExecution(); // do what we have to do to cleanup. + bool ContinueExecutionPrompt(); + nsresult DisplayConfirmationPrompt(nsIMsgWindow* msgWindow, + const char16_t* confirmString, + bool* confirmed); + nsCOMPtr<nsIMsgWindow> m_msgWindow; + nsCOMPtr<nsIMsgFilterList> m_filters; + nsTArray<RefPtr<nsIMsgFolder>> m_folders; + nsCOMPtr<nsIMsgFolder> m_curFolder; + nsCOMPtr<nsIMsgDatabase> m_curFolderDB; + nsCOMPtr<nsIMsgFilter> m_curFilter; + uint32_t m_curFilterIndex; + uint32_t m_curFolderIndex; + uint32_t m_numFilters; + nsTArray<nsMsgKey> m_searchHits; + nsTArray<RefPtr<nsIMsgDBHdr>> m_searchHitHdrs; + nsTArray<nsMsgKey> m_stopFiltering; + nsCOMPtr<nsIMsgSearchSession> m_searchSession; + nsCOMPtr<nsIMsgOperationListener> m_callback; + uint32_t m_nextAction; // next filter action to perform + nsresult mFinalResult; // report of overall success or failure + bool mNeedsRelease; // Did we need to release ourself? +}; + +NS_IMPL_ISUPPORTS(nsMsgFilterAfterTheFact, nsIUrlListener, nsIMsgSearchNotify, + nsIMsgCopyServiceListener) + +nsMsgFilterAfterTheFact::nsMsgFilterAfterTheFact( + nsIMsgWindow* aMsgWindow, nsIMsgFilterList* aFilterList, + const nsTArray<RefPtr<nsIMsgFolder>>& aFolderList, + nsIMsgOperationListener* aCallback) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, ("(Post) nsMsgFilterAfterTheFact")); + m_curFilterIndex = m_curFolderIndex = m_nextAction = 0; + m_msgWindow = aMsgWindow; + m_filters = aFilterList; + m_folders = aFolderList.Clone(); + m_filters->GetFilterCount(&m_numFilters); + + NS_ADDREF_THIS(); // we own ourselves, and will release ourselves when + // execution is done. + mNeedsRelease = true; + + m_callback = aCallback; + mFinalResult = NS_OK; +} + +nsMsgFilterAfterTheFact::~nsMsgFilterAfterTheFact() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) ~nsMsgFilterAfterTheFact")); +} + +// do what we have to do to cleanup. +nsresult nsMsgFilterAfterTheFact::OnEndExecution() { + if (m_searchSession) m_searchSession->UnregisterListener(this); + + if (m_filters) (void)m_filters->FlushLogIfNecessary(); + + if (m_callback) (void)m_callback->OnStopOperation(mFinalResult); + + nsresult rv = mFinalResult; + // OnEndExecution() can be called a second time when a rule execution fails + // and the user is prompted whether he wants to continue. + if (mNeedsRelease) { + NS_RELEASE_THIS(); // release ourselves. + mNeedsRelease = false; + } + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, ("(Post) End executing filters")); + return rv; +} + +nsresult nsMsgFilterAfterTheFact::RunNextFilter() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgFilterAfterTheFact::RunNextFilter")); + nsresult rv = NS_OK; + while (true) { + m_curFilter = nullptr; + if (m_curFilterIndex >= m_numFilters) break; + + BREAK_IF_FALSE(m_filters, "Missing filters"); + + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Running filter %" PRIu32, m_curFilterIndex)); + + rv = + m_filters->GetFilterAt(m_curFilterIndex++, getter_AddRefs(m_curFilter)); + CONTINUE_IF_FAILURE(rv, "Could not get filter at index"); + + nsString filterName; + m_curFilter->GetFilterName(filterName); + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Filter name: %s", NS_ConvertUTF16toUTF8(filterName).get())); + // clang-format on + + nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms; + rv = m_curFilter->GetSearchTerms(searchTerms); + CONTINUE_IF_FAILURE(rv, "Could not get searchTerms"); + + if (m_searchSession) m_searchSession->UnregisterListener(this); + m_searchSession = + do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv); + BREAK_IF_FAILURE(rv, "Failed to get search session"); + + nsMsgSearchScopeValue searchScope = nsMsgSearchScope::offlineMail; + for (nsIMsgSearchTerm* term : searchTerms) { + rv = m_searchSession->AppendTerm(term); + BREAK_IF_FAILURE(rv, "Could not append search term"); + } + CONTINUE_IF_FAILURE(rv, "Failed to setup search terms"); + m_searchSession->RegisterListener(this, + nsIMsgSearchSession::allNotifications); + + rv = m_searchSession->AddScopeTerm(searchScope, m_curFolder); + CONTINUE_IF_FAILURE(rv, "Failed to add scope term"); + m_nextAction = 0; + rv = m_searchSession->Search(m_msgWindow); + CONTINUE_IF_FAILURE(rv, "Search failed"); + return NS_OK; // OnSearchDone will continue + } + + if (NS_FAILED(rv)) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, + ("(Post) Filter evaluation failed")); + m_filters->LogFilterMessage(u"Filter evaluation failed"_ns, m_curFilter); + } + + m_curFilter = nullptr; + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Search failed"); + return AdvanceToNextFolder(); +} + +nsresult nsMsgFilterAfterTheFact::AdvanceToNextFolder() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgFilterAfterTheFact::AdvanceToNextFolder")); + nsresult rv = NS_OK; + // Advance through folders, making sure m_curFolder is null on errors + while (true) { + m_stopFiltering.Clear(); + m_curFolder = nullptr; + if (m_curFolderIndex >= m_folders.Length()) { + // final end of nsMsgFilterAfterTheFact object + return OnEndExecution(); + } + + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Entering folder %" PRIu32, m_curFolderIndex)); + + // reset the filter index to apply all filters to this new folder + m_curFilterIndex = 0; + m_nextAction = 0; + m_curFolder = m_folders[m_curFolderIndex++]; + + // Note: I got rv = NS_OK but null m_curFolder after deleting a folder + // outside of TB, when I select a single message and "run filter on message" + // and the filter is to move the message to the deleted folder. + + // m_curFolder may be null when the folder is deleted externally. + CONTINUE_IF_FALSE(m_curFolder, "Next folder returned null"); + + nsString folderName; + (void)m_curFolder->GetName(folderName); + MOZ_LOG( + FILTERLOGMODULE, LogLevel::Info, + ("(Post) Folder name: %s", NS_ConvertUTF16toUTF8(folderName).get())); + + nsCOMPtr<nsIFile> folderPath; + (void)m_curFolder->GetFilePath(getter_AddRefs(folderPath)); + (void)folderPath->GetPath(folderName); + MOZ_LOG( + FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Folder path: %s", NS_ConvertUTF16toUTF8(folderName).get())); + + rv = m_curFolder->GetMsgDatabase(getter_AddRefs(m_curFolderDB)); + if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) { + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = + do_QueryInterface(m_curFolder, &rv); + if (NS_SUCCEEDED(rv) && localFolder) + // will continue with OnStopRunningUrl + return localFolder->ParseFolder(m_msgWindow, this); + } + CONTINUE_IF_FAILURE(rv, "Could not get folder db"); + + rv = RunNextFilter(); + // RunNextFilter returns success when either filters are done, or an async + // process has started. It will call AdvanceToNextFolder itself if possible, + // so no need to call here. + BREAK_IF_FAILURE(rv, "Failed to run next filter"); + break; + } + return rv; +} + +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnStartRunningUrl(nsIURI* aUrl) { + return NS_OK; +} + +// This is the return from a folder parse +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnStopRunningUrl(nsIURI* aUrl, + nsresult aExitCode) { + if (NS_SUCCEEDED(aExitCode)) return RunNextFilter(); + + mFinalResult = aExitCode; + // If m_msgWindow then we are in a context where the user can deal with + // errors. Put up a prompt, and exit if user wants. + if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); + + // folder parse failed, so stop processing this folder. + return AdvanceToNextFolder(); +} + +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnSearchHit(nsIMsgDBHdr* header, + nsIMsgFolder* folder) { + NS_ENSURE_ARG_POINTER(header); + + nsMsgKey msgKey; + header->GetMessageKey(&msgKey); + + nsCString msgId; + header->GetMessageId(getter_Copies(msgId)); + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Filter matched message with key %" PRIu32, + msgKeyToInt(msgKey))); + // clang-format on + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Matched message ID: %s", msgId.get())); + + // Under various previous actions (a move, delete, or stopExecution) + // we do not want to process filters on a per-message basis. + if (m_stopFiltering.Contains(msgKey)) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Stopping further filter execution on this message")); + return NS_OK; + } + + m_searchHits.AppendElement(msgKey); + m_searchHitHdrs.AppendElement(header); + return NS_OK; +} + +// Continue after an async operation. +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnSearchDone(nsresult status) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Done matching current filter")); + if (NS_SUCCEEDED(status)) + return m_searchHits.IsEmpty() ? RunNextFilter() : ApplyFilter(); + + mFinalResult = status; + if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); + + // The search failed, so move on to the next filter. + return RunNextFilter(); +} + +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnNewSearch() { + m_searchHits.Clear(); + m_searchHitHdrs.Clear(); + return NS_OK; +} + +// This method will apply filters. It will continue to advance though headers, +// filters, and folders until done, unless it starts an async operation with +// a callback. The callback should call ApplyFilter again. It only returns +// an error if it is impossible to continue after attempting to continue the +// next filter action, filter, or folder. +nsresult nsMsgFilterAfterTheFact::ApplyFilter() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgFilterAfterTheFact::ApplyFilter")); + nsresult rv; + do { + // Error management block, break if unable to continue with filter. + + if (!m_curFilter) + break; // Maybe not an error, we just need to call RunNextFilter(); + if (!m_curFolder) + break; // Maybe not an error, we just need to call AdvanceToNextFolder(); + + // 'm_curFolder' can be reset asynchronously by the copy service + // calling OnStopCopy(). So take a local copy here and use it throughout the + // function. + nsCOMPtr<nsIMsgFolder> curFolder = m_curFolder; + nsCOMPtr<nsIMsgFilter> curFilter = m_curFilter; + + // We're going to log the filter actions before firing them because some + // actions are async. + bool loggingEnabled = false; + if (m_filters) (void)m_filters->GetLoggingEnabled(&loggingEnabled); + + nsTArray<RefPtr<nsIMsgRuleAction>> actionList; + rv = curFilter->GetSortedActionList(actionList); + BREAK_IF_FAILURE(rv, "Could not get action list for filter"); + + uint32_t numActions = actionList.Length(); + + if (m_nextAction == 0) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Applying %" PRIu32 " filter actions to %" PRIu32 + " matched messages", + numActions, static_cast<uint32_t>(m_searchHits.Length()))); + } else if (m_nextAction < numActions) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Applying remaining %" PRIu32 + " filter actions to %" PRIu32 " matched messages", + numActions - m_nextAction, + static_cast<uint32_t>(m_searchHits.Length()))); + } + + // We start from m_nextAction to allow us to continue applying actions + // after the return from an async copy. + while (m_nextAction < numActions) { + nsresult finalResult = NS_OK; + nsCOMPtr<nsIMsgRuleAction> filterAction(actionList[m_nextAction]); + ++m_nextAction; + + nsMsgRuleActionType actionType; + rv = filterAction->GetType(&actionType); + CONTINUE_IF_FAILURE(rv, "Could not get type for filter action"); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Running filter action at index %" PRIu32 + ", action type = %i", + m_nextAction - 1, actionType)); + + nsCString actionTargetFolderUri; + if (actionType == nsMsgFilterAction::MoveToFolder || + actionType == nsMsgFilterAction::CopyToFolder) { + rv = filterAction->GetTargetFolderUri(actionTargetFolderUri); + CONTINUE_IF_FAILURE(rv, "GetTargetFolderUri failed"); + CONTINUE_IF_FALSE(!actionTargetFolderUri.IsEmpty(), + "actionTargetFolderUri is empty"); + } + + if (loggingEnabled) { + for (auto msgHdr : m_searchHitHdrs) { + (void)curFilter->LogRuleHit(filterAction, msgHdr); + } + } + + // all actions that pass "this" as a listener in order to chain filter + // execution when the action is finished need to return before reaching + // the bottom of this routine, because we run the next filter at the end + // of this routine. + switch (actionType) { + case nsMsgFilterAction::Delete: + // we can't pass ourselves in as a copy service listener because the + // copy service listener won't get called in several situations (e.g., + // the delete model is imap delete) and we rely on the listener + // getting called to continue the filter application. This means we're + // going to end up firing off the delete, and then subsequently + // issuing a search for the next filter, which will block until the + // delete finishes. + rv = curFolder->DeleteMessages(m_searchHitHdrs, m_msgWindow, false, + false, nullptr, false /*allow Undo*/); + BREAK_ACTION_IF_FAILURE(rv, "Deleting messages failed"); + + // don't allow any more filters on this message + m_stopFiltering.AppendElements(m_searchHits); + for (uint32_t i = 0; i < m_searchHits.Length(); i++) + curFolder->OrProcessingFlags(m_searchHits[i], + nsMsgProcessingFlags::FilterToMove); + // if we are deleting then we couldn't care less about applying + // remaining filter actions + m_nextAction = numActions; + break; + + case nsMsgFilterAction::MoveToFolder: + // Even if move fails we will not run additional actions, as they + // would not have run if move succeeded. + m_nextAction = numActions; + // Fall through to the copy case. + [[fallthrough]]; + case nsMsgFilterAction::CopyToFolder: { + nsCString uri; + curFolder->GetURI(uri); + + if (uri.Equals(actionTargetFolderUri)) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Target folder is the same as source folder, " + "skipping")); + break; + } + + nsCOMPtr<nsIMsgFolder> destIFolder; + rv = GetOrCreateFolder(actionTargetFolderUri, + getter_AddRefs(destIFolder)); + BREAK_ACTION_IF_FAILURE(rv, "Could not get action folder"); + + bool canFileMessages = true; + nsCOMPtr<nsIMsgFolder> parentFolder; + destIFolder->GetParent(getter_AddRefs(parentFolder)); + if (parentFolder) destIFolder->GetCanFileMessages(&canFileMessages); + if (!parentFolder || !canFileMessages) { + curFilter->SetEnabled(false); + destIFolder->ThrowAlertMsg("filterDisabled", m_msgWindow); + // we need to explicitly save the filter file. + m_filters->SaveToDefaultFile(); + // In the case of applying multiple filters + // we might want to remove the filter from the list, but + // that's a bit evil since we really don't know that we own + // the list. Disabling it doesn't do a lot of good since + // we still apply disabled filters. Currently, we don't + // have any clients that apply filters to multiple folders, + // so this might be the edge case of an edge case. + m_nextAction = numActions; + BREAK_ACTION_IF_FALSE(false, + "No parent folder or folder can't file " + "messages, disabling the filter"); + } + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv); + BREAK_ACTION_IF_FAILURE(rv, "Could not get copy service"); + + if (actionType == nsMsgFilterAction::MoveToFolder) { + m_stopFiltering.AppendElements(m_searchHits); + for (uint32_t i = 0; i < m_searchHits.Length(); i++) + curFolder->OrProcessingFlags(m_searchHits[i], + nsMsgProcessingFlags::FilterToMove); + } + + rv = copyService->CopyMessages( + curFolder, m_searchHitHdrs, destIFolder, + actionType == nsMsgFilterAction::MoveToFolder, this, m_msgWindow, + false); + BREAK_ACTION_IF_FAILURE(rv, "CopyMessages failed"); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Action execution continues async")); + return NS_OK; // OnStopCopy callback to continue; + } break; + case nsMsgFilterAction::MarkRead: + // crud, no listener support here - we'll probably just need to go on + // and apply the next filter, and, in the imap case, rely on multiple + // connection and url queueing to stay out of trouble + rv = curFolder->MarkMessagesRead(m_searchHitHdrs, true); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + break; + case nsMsgFilterAction::MarkUnread: + rv = curFolder->MarkMessagesRead(m_searchHitHdrs, false); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + break; + case nsMsgFilterAction::MarkFlagged: + rv = curFolder->MarkMessagesFlagged(m_searchHitHdrs, true); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + break; + case nsMsgFilterAction::KillThread: + case nsMsgFilterAction::WatchThread: { + for (auto msgHdr : m_searchHitHdrs) { + nsCOMPtr<nsIMsgThread> msgThread; + nsMsgKey threadKey; + m_curFolderDB->GetThreadContainingMsgHdr(msgHdr, + getter_AddRefs(msgThread)); + BREAK_ACTION_IF_FALSE(msgThread, "Could not find msg thread"); + msgThread->GetThreadKey(&threadKey); + if (actionType == nsMsgFilterAction::KillThread) { + rv = m_curFolderDB->MarkThreadIgnored(msgThread, threadKey, true, + nullptr); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + } else { + rv = m_curFolderDB->MarkThreadWatched(msgThread, threadKey, true, + nullptr); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + } + } + } break; + case nsMsgFilterAction::KillSubthread: { + for (auto msgHdr : m_searchHitHdrs) { + rv = m_curFolderDB->MarkHeaderKilled(msgHdr, true, nullptr); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + } + } break; + case nsMsgFilterAction::ChangePriority: { + nsMsgPriorityValue filterPriority; + filterAction->GetPriority(&filterPriority); + for (auto msgHdr : m_searchHitHdrs) { + rv = msgHdr->SetPriority(filterPriority); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + } + } break; + case nsMsgFilterAction::AddTag: { + nsCString keyword; + filterAction->GetStrValue(keyword); + rv = curFolder->AddKeywordsToMessages(m_searchHitHdrs, keyword); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + } break; + case nsMsgFilterAction::JunkScore: { + nsAutoCString junkScoreStr; + int32_t junkScore; + filterAction->GetJunkScore(&junkScore); + junkScoreStr.AppendInt(junkScore); + rv = + curFolder->SetJunkScoreForMessages(m_searchHitHdrs, junkScoreStr); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + } break; + case nsMsgFilterAction::Forward: { + nsCOMPtr<nsIMsgIncomingServer> server; + rv = curFolder->GetServer(getter_AddRefs(server)); + BREAK_ACTION_IF_FAILURE(rv, "Could not get server"); + nsCString forwardTo; + filterAction->GetStrValue(forwardTo); + BREAK_ACTION_IF_FALSE(!forwardTo.IsEmpty(), "blank forwardTo URI"); + nsCOMPtr<nsIMsgComposeService> compService = + do_GetService("@mozilla.org/messengercompose;1", &rv); + BREAK_ACTION_IF_FAILURE(rv, "Could not get compose service"); + + for (auto msgHdr : m_searchHitHdrs) { + rv = compService->ForwardMessage( + NS_ConvertASCIItoUTF16(forwardTo), msgHdr, m_msgWindow, server, + nsIMsgComposeService::kForwardAsDefault); + BREAK_ACTION_IF_FAILURE(rv, "Forward action failed"); + } + } break; + case nsMsgFilterAction::Reply: { + nsCString replyTemplateUri; + filterAction->GetStrValue(replyTemplateUri); + BREAK_ACTION_IF_FALSE(!replyTemplateUri.IsEmpty(), + "Empty reply template URI"); + + nsCOMPtr<nsIMsgIncomingServer> server; + rv = curFolder->GetServer(getter_AddRefs(server)); + BREAK_ACTION_IF_FAILURE(rv, "Could not get server"); + + nsCOMPtr<nsIMsgComposeService> compService = + do_GetService("@mozilla.org/messengercompose;1", &rv); + BREAK_ACTION_IF_FAILURE(rv, "Could not get compose service"); + for (auto msgHdr : m_searchHitHdrs) { + rv = compService->ReplyWithTemplate(msgHdr, replyTemplateUri, + m_msgWindow, server); + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_ABORT) { + (void)curFilter->LogRuleHitFail( + filterAction, msgHdr, rv, + "filterFailureSendingReplyAborted"_ns); + } else { + (void)curFilter->LogRuleHitFail( + filterAction, msgHdr, rv, + "filterFailureSendingReplyError"_ns); + } + } + BREAK_ACTION_IF_FAILURE(rv, "ReplyWithTemplate failed"); + } + } break; + case nsMsgFilterAction::DeleteFromPop3Server: { + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = + do_QueryInterface(curFolder, &rv); + BREAK_ACTION_IF_FAILURE(rv, "Current folder not a local folder"); + BREAK_ACTION_IF_FALSE(localFolder, + "Current folder not a local folder"); + // This action ignores the deleteMailLeftOnServer preference + rv = localFolder->MarkMsgsOnPop3Server(m_searchHitHdrs, + POP3_FORCE_DEL); + BREAK_ACTION_IF_FAILURE(rv, "MarkMsgsOnPop3Server failed"); + + // Delete the partial headers. They're useless now + // that the server copy is being deleted. + nsTArray<RefPtr<nsIMsgDBHdr>> partialMsgs; + for (uint32_t i = 0; i < m_searchHits.Length(); ++i) { + nsIMsgDBHdr* msgHdr = m_searchHitHdrs[i]; + nsMsgKey msgKey = m_searchHits[i]; + uint32_t flags; + msgHdr->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Partial) { + partialMsgs.AppendElement(msgHdr); + m_stopFiltering.AppendElement(msgKey); + curFolder->OrProcessingFlags(msgKey, + nsMsgProcessingFlags::FilterToMove); + } + } + if (!partialMsgs.IsEmpty()) { + rv = curFolder->DeleteMessages(partialMsgs, m_msgWindow, true, + false, nullptr, false); + BREAK_ACTION_IF_FAILURE(rv, "Delete messages failed"); + } + } break; + case nsMsgFilterAction::FetchBodyFromPop3Server: { + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = + do_QueryInterface(curFolder, &rv); + BREAK_ACTION_IF_FAILURE(rv, "current folder not local"); + BREAK_ACTION_IF_FALSE(localFolder, "current folder not local"); + nsTArray<RefPtr<nsIMsgDBHdr>> messages; + for (nsIMsgDBHdr* msgHdr : m_searchHitHdrs) { + uint32_t flags = 0; + msgHdr->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Partial) + messages.AppendElement(msgHdr); + } + if (messages.Length() > 0) { + rv = curFolder->DownloadMessagesForOffline(messages, m_msgWindow); + BREAK_ACTION_IF_FAILURE(rv, "DownloadMessagesForOffline failed"); + } + } break; + + case nsMsgFilterAction::StopExecution: { + // don't apply any more filters + m_stopFiltering.AppendElements(m_searchHits); + m_nextAction = numActions; + } break; + + case nsMsgFilterAction::Custom: { + nsMsgFilterTypeType filterType; + curFilter->GetFilterType(&filterType); + nsCOMPtr<nsIMsgFilterCustomAction> customAction; + rv = filterAction->GetCustomAction(getter_AddRefs(customAction)); + BREAK_ACTION_IF_FAILURE(rv, "Could not get custom action"); + + nsAutoCString value; + rv = filterAction->GetStrValue(value); + BREAK_ACTION_IF_FAILURE(rv, "Could not get custom action value"); + bool isAsync = false; + customAction->GetIsAsync(&isAsync); + rv = customAction->ApplyAction(m_searchHitHdrs, value, this, + filterType, m_msgWindow); + BREAK_ACTION_IF_FAILURE(rv, "custom action failed to apply"); + if (isAsync) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Action execution continues async")); + return NS_OK; // custom action should call ApplyFilter on callback + } + } break; + + default: + NS_ERROR("unexpected filter action"); + BREAK_ACTION_IF_FAILURE(NS_ERROR_UNEXPECTED, + "Unexpected filter action"); + } + if (NS_FAILED(finalResult)) { + mFinalResult = finalResult; + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, + ("(Post) Action execution failed with error: %" PRIx32, + static_cast<uint32_t>(mFinalResult))); + if (loggingEnabled && m_searchHitHdrs.Length() > 0) { + (void)curFilter->LogRuleHitFail(filterAction, m_searchHitHdrs[0], + mFinalResult, + "filterActionFailed"_ns); + } + } else { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Action execution succeeded")); + } + } + } while (false); // end error management block + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Finished executing actions")); + return RunNextFilter(); +} + +NS_IMETHODIMP nsMsgFilterService::GetTempFilterList( + nsIMsgFolder* aFolder, nsIMsgFilterList** aFilterList) { + NS_ENSURE_ARG_POINTER(aFilterList); + + nsMsgFilterList* filterList = new nsMsgFilterList; + filterList->SetFolder(aFolder); + filterList->m_temporaryList = true; + NS_ADDREF(*aFilterList = filterList); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilterService::ApplyFiltersToFolders( + nsIMsgFilterList* aFilterList, + const nsTArray<RefPtr<nsIMsgFolder>>& aFolders, nsIMsgWindow* aMsgWindow, + nsIMsgOperationListener* aCallback) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgFilterService::ApplyFiltersToFolders")); + NS_ENSURE_ARG_POINTER(aFilterList); + + uint32_t filterCount; + aFilterList->GetFilterCount(&filterCount); + nsCString listId; + aFilterList->GetListId(listId); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Manual filter run initiated")); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Running %" PRIu32 " filters from %s on %" PRIu32 " folders", + filterCount, listId.get(), (int)aFolders.Length())); + + RefPtr<nsMsgFilterAfterTheFact> filterExecutor = + new nsMsgFilterAfterTheFact(aMsgWindow, aFilterList, aFolders, aCallback); + if (filterExecutor) + return filterExecutor->AdvanceToNextFolder(); + else + return NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP nsMsgFilterService::AddCustomAction( + nsIMsgFilterCustomAction* aAction) { + mCustomActions.AppendElement(aAction); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterService::GetCustomActions( + nsTArray<RefPtr<nsIMsgFilterCustomAction>>& actions) { + actions = mCustomActions.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilterService::GetCustomAction(const nsACString& aId, + nsIMsgFilterCustomAction** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + for (nsIMsgFilterCustomAction* action : mCustomActions) { + nsAutoCString id; + nsresult rv = action->GetId(id); + if (NS_SUCCEEDED(rv) && aId.Equals(id)) { + NS_ADDREF(*aResult = action); + return NS_OK; + } + } + aResult = nullptr; + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsMsgFilterService::AddCustomTerm(nsIMsgSearchCustomTerm* aTerm) { + mCustomTerms.AppendElement(aTerm); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterService::GetCustomTerms( + nsTArray<RefPtr<nsIMsgSearchCustomTerm>>& terms) { + terms = mCustomTerms.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilterService::GetCustomTerm(const nsACString& aId, + nsIMsgSearchCustomTerm** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + for (nsIMsgSearchCustomTerm* term : mCustomTerms) { + nsAutoCString id; + nsresult rv = term->GetId(id); + if (NS_SUCCEEDED(rv) && aId.Equals(id)) { + NS_ADDREF(*aResult = term); + return NS_OK; + } + } + aResult = nullptr; + // we use a null result to indicate failure to find a term + return NS_OK; +} + +/** + * Translate the filter type flag into human readable type names. + * In case of multiple flag they are delimited by '&'. + */ +NS_IMETHODIMP +nsMsgFilterService::FilterTypeName(nsMsgFilterTypeType filterType, + nsACString& typeName) { + typeName.Truncate(); + if (filterType == nsMsgFilterType::None) { + typeName.Assign("None"); + return NS_OK; + } + + if ((filterType & nsMsgFilterType::Incoming) == nsMsgFilterType::Incoming) { + typeName.Append("Incoming&"); + } else { + if ((filterType & nsMsgFilterType::Inbox) == nsMsgFilterType::Inbox) { + typeName.Append("Inbox&"); + } else { + if (filterType & nsMsgFilterType::InboxRule) + typeName.Append("InboxRule&"); + if (filterType & nsMsgFilterType::InboxJavaScript) + typeName.Append("InboxJavaScript&"); + } + if ((filterType & nsMsgFilterType::News) == nsMsgFilterType::News) { + typeName.Append("News&"); + } else { + if (filterType & nsMsgFilterType::NewsRule) typeName.Append("NewsRule&"); + if (filterType & nsMsgFilterType::NewsJavaScript) + typeName.Append("NewsJavaScript&"); + } + } + if (filterType & nsMsgFilterType::Manual) typeName.Append("Manual&"); + if (filterType & nsMsgFilterType::PostPlugin) typeName.Append("PostPlugin&"); + if (filterType & nsMsgFilterType::PostOutgoing) + typeName.Append("PostOutgoing&"); + if (filterType & nsMsgFilterType::Archive) typeName.Append("Archive&"); + if (filterType & nsMsgFilterType::Periodic) typeName.Append("Periodic&"); + + if (typeName.IsEmpty()) { + typeName.Assign("UNKNOWN"); + } else { + // Cut the trailing '&' character. + typeName.Truncate(typeName.Length() - 1); + } + return NS_OK; +} + +// nsMsgApplyFiltersToMessages overrides nsMsgFilterAfterTheFact in order to +// apply filters to a list of messages, rather than an entire folder +class nsMsgApplyFiltersToMessages : public nsMsgFilterAfterTheFact { + public: + nsMsgApplyFiltersToMessages(nsIMsgWindow* aMsgWindow, + nsIMsgFilterList* aFilterList, + const nsTArray<RefPtr<nsIMsgFolder>>& aFolderList, + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgHdrList, + nsMsgFilterTypeType aFilterType, + nsIMsgOperationListener* aCallback); + + protected: + virtual nsresult RunNextFilter(); + + nsTArray<RefPtr<nsIMsgDBHdr>> m_msgHdrList; + nsMsgFilterTypeType m_filterType; +}; + +nsMsgApplyFiltersToMessages::nsMsgApplyFiltersToMessages( + nsIMsgWindow* aMsgWindow, nsIMsgFilterList* aFilterList, + const nsTArray<RefPtr<nsIMsgFolder>>& aFolderList, + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgHdrList, + nsMsgFilterTypeType aFilterType, nsIMsgOperationListener* aCallback) + : nsMsgFilterAfterTheFact(aMsgWindow, aFilterList, aFolderList, aCallback), + m_msgHdrList(aMsgHdrList.Clone()), + m_filterType(aFilterType) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgApplyFiltersToMessages")); +} + +nsresult nsMsgApplyFiltersToMessages::RunNextFilter() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgApplyFiltersToMessages::RunNextFilter")); + nsresult rv = NS_OK; + while (true) { + m_curFilter = nullptr; // we are done with the current filter + if (!m_curFolder || // Not an error, we just need to run + // AdvanceToNextFolder() + m_curFilterIndex >= m_numFilters) + break; + + BREAK_IF_FALSE(m_filters, "No filters"); + nsMsgFilterTypeType filterType; + bool isEnabled; + rv = + m_filters->GetFilterAt(m_curFilterIndex++, getter_AddRefs(m_curFilter)); + CONTINUE_IF_FAILURE(rv, "Could not get filter"); + rv = m_curFilter->GetFilterType(&filterType); + CONTINUE_IF_FAILURE(rv, "Could not get filter type"); + if (!(filterType & m_filterType)) continue; + rv = m_curFilter->GetEnabled(&isEnabled); + CONTINUE_IF_FAILURE(rv, "Could not get isEnabled"); + if (!isEnabled) continue; + + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Running filter %" PRIu32, m_curFilterIndex)); + nsString filterName; + m_curFilter->GetFilterName(filterName); + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Filter name: %s", NS_ConvertUTF16toUTF8(filterName).get())); + // clang-format on + + nsCOMPtr<nsIMsgSearchScopeTerm> scope(new nsMsgSearchScopeTerm( + nullptr, nsMsgSearchScope::offlineMail, m_curFolder)); + BREAK_IF_FALSE(scope, "Could not create scope, OOM?"); + m_curFilter->SetScope(scope); + OnNewSearch(); + + for (auto msgHdr : m_msgHdrList) { + bool matched; + rv = m_curFilter->MatchHdr(msgHdr, m_curFolder, m_curFolderDB, + EmptyCString(), &matched); + if (NS_SUCCEEDED(rv) && matched) { + // In order to work with nsMsgFilterAfterTheFact::ApplyFilter we + // initialize nsMsgFilterAfterTheFact's information with a search hit + // now for the message that we're filtering. + OnSearchHit(msgHdr, m_curFolder); + } + } + m_curFilter->SetScope(nullptr); + + if (m_searchHits.Length() > 0) { + m_nextAction = 0; + rv = ApplyFilter(); + if (NS_SUCCEEDED(rv)) + return NS_OK; // async callback will continue, or we are done. + } + } + + if (NS_FAILED(rv)) { + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, + ("(Post) Filter run failed (%" PRIx32 ")", + static_cast<uint32_t>(rv))); + // clang-format on + m_filters->LogFilterMessage(u"Filter run failed"_ns, m_curFilter); + NS_WARNING_ASSERTION(false, "Failed to run filters"); + } else { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Filter run finished on the current folder")); + } + + m_curFilter = nullptr; + + // We expect the failure is already recorded through one of the macro + // expressions, that will have console logging added to them. + // So an additional console warning is not needed here. + return AdvanceToNextFolder(); +} + +NS_IMETHODIMP nsMsgFilterService::ApplyFilters( + nsMsgFilterTypeType aFilterType, + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgHdrList, nsIMsgFolder* aFolder, + nsIMsgWindow* aMsgWindow, nsIMsgOperationListener* aCallback) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgApplyFiltersToMessages::ApplyFilters")); + NS_ENSURE_ARG_POINTER(aFolder); + + nsCOMPtr<nsIMsgFilterList> filterList; + aFolder->GetFilterList(aMsgWindow, getter_AddRefs(filterList)); + NS_ENSURE_STATE(filterList); + + uint32_t filterCount; + filterList->GetFilterCount(&filterCount); + nsCString listId; + filterList->GetListId(listId); + nsString folderName; + aFolder->GetName(folderName); + nsCString typeName; + FilterTypeName(aFilterType, typeName); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Filter run initiated, trigger=%s (%i)", typeName.get(), + aFilterType)); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Running %" PRIu32 " filters from %s on %" PRIu32 + " message(s) in folder '%s'", + filterCount, listId.get(), (uint32_t)aMsgHdrList.Length(), + NS_ConvertUTF16toUTF8(folderName).get())); + + // Create our nsMsgApplyFiltersToMessages object which will be called when + // ApplyFiltersToHdr finds one or more filters that hit. + RefPtr<nsMsgApplyFiltersToMessages> filterExecutor = + new nsMsgApplyFiltersToMessages(aMsgWindow, filterList, {aFolder}, + aMsgHdrList, aFilterType, aCallback); + + if (filterExecutor) return filterExecutor->AdvanceToNextFolder(); + + return NS_ERROR_OUT_OF_MEMORY; +} + +/* void OnStartCopy (); */ +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnStartCopy() { return NS_OK; } + +/* void OnProgress (in uint32_t aProgress, in uint32_t aProgressMax); */ +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnProgress(uint32_t aProgress, + uint32_t aProgressMax) { + return NS_OK; +} + +/* void SetMessageKey (in uint32_t aKey); */ +NS_IMETHODIMP nsMsgFilterAfterTheFact::SetMessageKey(nsMsgKey /* aKey */) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterAfterTheFact::GetMessageId(nsACString& messageId) { + return NS_OK; +} + +/* void OnStopCopy (in nsresult aStatus); */ +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnStopCopy(nsresult aStatus) { + if (NS_SUCCEEDED(aStatus)) { + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Async message copy from filter action finished successfully")); + // clang-format on + return ApplyFilter(); + } + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, + ("(Post) Async message copy from filter action failed (%" PRIx32 ")", + static_cast<uint32_t>(aStatus))); + + mFinalResult = aStatus; + if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); + + // Copy failed, so run the next filter + return RunNextFilter(); +} + +bool nsMsgFilterAfterTheFact::ContinueExecutionPrompt() { + if (!m_curFilter) return false; + nsCOMPtr<nsIStringBundle> bundle; + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + if (!bundleService) return false; + bundleService->CreateBundle("chrome://messenger/locale/filter.properties", + getter_AddRefs(bundle)); + if (!bundle) return false; + nsString filterName; + m_curFilter->GetFilterName(filterName); + nsString formatString; + nsString confirmText; + AutoTArray<nsString, 1> formatStrings = {filterName}; + nsresult rv = bundle->FormatStringFromName("continueFilterExecution", + formatStrings, confirmText); + if (NS_FAILED(rv)) return false; + bool returnVal = false; + (void)DisplayConfirmationPrompt(m_msgWindow, confirmText.get(), &returnVal); + if (!returnVal) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning, + ("(Post) User aborted further filter execution on prompt")); + } + return returnVal; +} + +nsresult nsMsgFilterAfterTheFact::DisplayConfirmationPrompt( + nsIMsgWindow* msgWindow, const char16_t* confirmString, bool* confirmed) { + if (msgWindow) { + nsCOMPtr<nsIDocShell> docShell; + msgWindow->GetRootDocShell(getter_AddRefs(docShell)); + if (docShell) { + nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell)); + if (dialog && confirmString) + dialog->Confirm(nullptr, confirmString, confirmed); + } + } + return NS_OK; +} diff --git a/comm/mailnews/search/src/nsMsgFilterService.h b/comm/mailnews/search/src/nsMsgFilterService.h new file mode 100644 index 0000000000..7089c8362f --- /dev/null +++ b/comm/mailnews/search/src/nsMsgFilterService.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgFilterService_H_ +#define _nsMsgFilterService_H_ + +#include "nsIMsgFilterService.h" +#include "nsIFile.h" +#include "nsTArray.h" + +class nsIMsgWindow; +class nsIStringBundle; + +// The filter service is used to acquire and manipulate filter lists. + +class nsMsgFilterService : public nsIMsgFilterService { + public: + nsMsgFilterService(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGFILTERSERVICE + // clients call OpenFilterList to get a handle to a FilterList, of existing + // nsMsgFilter. These are manipulated by the front end as a result of user + // interaction with dialog boxes. To apply the new list call + // MSG_CloseFilterList. + + nsresult BackUpFilterFile(nsIFile* aFilterFile, nsIMsgWindow* aMsgWindow); + nsresult AlertBackingUpFilterFile(nsIMsgWindow* aMsgWindow); + nsresult ThrowAlertMsg(const char* aMsgName, nsIMsgWindow* aMsgWindow); + nsresult GetStringFromBundle(const char* aMsgName, nsAString& aResult); + nsresult GetFilterStringBundle(nsIStringBundle** aBundle); + + protected: + virtual ~nsMsgFilterService(); + + // defined custom action list + nsTArray<RefPtr<nsIMsgFilterCustomAction>> mCustomActions; + // defined custom term list + nsTArray<RefPtr<nsIMsgSearchCustomTerm>> mCustomTerms; +}; + +#endif // _nsMsgFilterService_H_ diff --git a/comm/mailnews/search/src/nsMsgImapSearch.cpp b/comm/mailnews/search/src/nsMsgImapSearch.cpp new file mode 100644 index 0000000000..9ca0183d60 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgImapSearch.cpp @@ -0,0 +1,991 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "msgCore.h" +#include "nsMsgSearchAdapter.h" +#include "nsMsgSearchScopeTerm.h" +#include "nsMsgResultElement.h" +#include "nsMsgSearchTerm.h" +#include "nsIMsgHdr.h" +#include "nsMsgSearchImap.h" +#include "prmem.h" +#include "nsIMsgImapMailFolder.h" +// Implementation of search for IMAP mail folders + +nsMsgSearchOnlineMail::nsMsgSearchOnlineMail( + nsMsgSearchScopeTerm* scope, + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList) + : nsMsgSearchAdapter(scope, termList) {} + +nsMsgSearchOnlineMail::~nsMsgSearchOnlineMail() {} + +nsresult nsMsgSearchOnlineMail::ValidateTerms() { + nsresult err = nsMsgSearchAdapter::ValidateTerms(); + + if (NS_SUCCEEDED(err)) { + // ### mwelch Figure out the charsets to use + // for the search terms and targets. + nsAutoString srcCharset, dstCharset; + GetSearchCharsets(srcCharset, dstCharset); + + // do IMAP specific validation + err = Encode(m_encoding, m_searchTerms, dstCharset.get(), m_scope); + NS_ASSERTION(NS_SUCCEEDED(err), "failed to encode imap search"); + } + + return err; +} + +NS_IMETHODIMP nsMsgSearchOnlineMail::GetEncoding(char** result) { + *result = ToNewCString(m_encoding); + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchOnlineMail::AddResultElement(nsIMsgDBHdr* pHeaders) { + nsresult err = NS_OK; + + nsCOMPtr<nsIMsgSearchSession> searchSession; + m_scope->GetSearchSession(getter_AddRefs(searchSession)); + if (searchSession) { + nsCOMPtr<nsIMsgFolder> scopeFolder; + err = m_scope->GetFolder(getter_AddRefs(scopeFolder)); + searchSession->AddSearchHit(pHeaders, scopeFolder); + } + // XXXX alecf do not checkin without fixing! + // m_scope->m_searchSession->AddResultElement (newResult); + return err; +} + +nsresult nsMsgSearchOnlineMail::Search(bool* aDone) { + // we should never end up here for a purely online + // folder. We might for an offline IMAP folder. + nsresult err = NS_ERROR_NOT_IMPLEMENTED; + + return err; +} + +nsresult nsMsgSearchOnlineMail::Encode( + nsCString& pEncoding, nsTArray<RefPtr<nsIMsgSearchTerm>> const& searchTerms, + const char16_t* destCharset, nsIMsgSearchScopeTerm* scope) { + nsCString imapTerms; + + // check if searchTerms are ascii only + bool asciiOnly = true; + // ### what's this mean in the NWO????? + + if (true) // !(srcCharset & CODESET_MASK == STATEFUL || srcCharset & + // CODESET_MASK == WIDECHAR) ) + // assume all single/multiple bytes charset has ascii as subset + { + for (nsIMsgSearchTerm* pTerm : searchTerms) { + nsMsgSearchAttribValue attribute; + pTerm->GetAttrib(&attribute); + if (IS_STRING_ATTRIBUTE(attribute)) { + nsString pchar; + nsCOMPtr<nsIMsgSearchValue> searchValue; + + nsresult rv = pTerm->GetValue(getter_AddRefs(searchValue)); + if (NS_FAILED(rv) || !searchValue) continue; + + rv = searchValue->GetStr(pchar); + if (NS_FAILED(rv) || pchar.IsEmpty()) continue; + asciiOnly = mozilla::IsAsciiNullTerminated( + static_cast<const char16_t*>(pchar.get())); + if (!asciiOnly) { + break; + } + } + } + } + // else + // asciiOnly = false; // TODO: enable this line when the condition is not a + // plain "true" in the if(). + + const char16_t* usAsciiCharSet = u"us-ascii"; + // Get the optional CHARSET parameter, in case we need it. + char* csname = GetImapCharsetParam(asciiOnly ? usAsciiCharSet : destCharset); + + // We do not need "srcCharset" since the search term in always unicode. + // I just pass destCharset for both src and dest charset instead of removing + // srcCharst from the argument. + nsresult err = nsMsgSearchAdapter::EncodeImap( + getter_Copies(imapTerms), searchTerms, + asciiOnly ? usAsciiCharSet : destCharset, + asciiOnly ? usAsciiCharSet : destCharset, false); + if (NS_SUCCEEDED(err)) { + pEncoding.AppendLiteral("SEARCH"); + if (csname) { + // We have a "CHARSET <name>" string which is typically appended to + // "SEARCH". But don't append it if server has UTF8=ACCEPT enabled. + nsCOMPtr<nsIMsgFolder> folder; + err = scope->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(err, err); + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder); + bool utf8AcceptEnabled = false; + imapFolder->GetShouldUseUtf8FolderName(&utf8AcceptEnabled); + if (!utf8AcceptEnabled) pEncoding.Append(csname); + } + pEncoding.Append(imapTerms); + } + PR_FREEIF(csname); + return err; +} + +// clang-format off +nsresult +nsMsgSearchValidityManager::InitOfflineMailTable() +{ + NS_ASSERTION(!m_offlineMailTable, "offline mail table already initted"); + nsresult rv = NewTable(getter_AddRefs(m_offlineMailTable)); + NS_ENSURE_SUCCESS(rv,rv); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1); + + // m_offlineMailTable->SetValidButNotShown (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsHigherThan, 1); + // m_offlineMailTable->SetValidButNotShown (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLowerThan, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsEmpty, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsEmpty, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsntEmpty, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsntEmpty, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Isnt, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::Is, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1); + m_offlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1); + m_offlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1); + return rv; +} + + +nsresult +nsMsgSearchValidityManager::InitOnlineMailTable() +{ + NS_ASSERTION(!m_onlineMailTable, "Online mail table already initted!"); + nsresult rv = NewTable(getter_AddRefs(m_onlineMailTable)); + NS_ENSURE_SUCCESS(rv,rv); + + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1); + + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1); + + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1); + + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1); + + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1); + + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1); + + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); + + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1); + + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + m_onlineMailTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + + return rv; +} + +nsresult +nsMsgSearchValidityManager::InitOnlineMailFilterTable() +{ + // Oh what a tangled web... + // + // IMAP filtering happens on the client, fundamentally using the same + // capabilities as POP filtering. However, since we don't yet have the + // IMAP message body, we can't filter on body attributes. So this table + // is supposed to be the same as offline mail, except that the body + // attribute is omitted + NS_ASSERTION(!m_onlineMailFilterTable, "online filter table already initted"); + nsresult rv = NewTable(getter_AddRefs(m_onlineMailFilterTable)); + NS_ENSURE_SUCCESS(rv,rv); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1); + + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_onlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + m_onlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + + return rv; +} + +nsresult +nsMsgSearchValidityManager::InitOfflineMailFilterTable() +{ + NS_ASSERTION(!m_offlineMailFilterTable, "offline mail filter table already initted"); + nsresult rv = NewTable(getter_AddRefs(m_offlineMailFilterTable)); + NS_ENSURE_SUCCESS(rv,rv); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1); + + // junk status and attachment status not available for offline mail (POP) filters + // because we won't know those until after the message has been analyzed. + // see bug #185937 + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::Is, 1); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1); + + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_offlineMailFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + m_offlineMailFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + + return rv; +} + +// Online Manual is used for IMAP and NEWS, where at manual +// filtering we have junk info, but cannot assure that the +// body is available. +nsresult +nsMsgSearchValidityManager::InitOnlineManualFilterTable() +{ + NS_ASSERTION(!m_onlineManualFilterTable, "online manual filter table already initted"); + nsresult rv = NewTable(getter_AddRefs(m_onlineManualFilterTable)); + NS_ENSURE_SUCCESS(rv, rv); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::EndsWith, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsInAB, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::To, nsMsgSearchOp::IsntInAB, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::EndsWith, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsInAB, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::CC, nsMsgSearchOp::IsntInAB, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::EndsWith, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsInAB, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::ToOrCC, nsMsgSearchOp::IsntInAB, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::EndsWith, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsInAB, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AllAddresses, nsMsgSearchOp::IsntInAB, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsHigherThan, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::IsLowerThan, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Priority, nsMsgSearchOp::Isnt, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsEmpty, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsEmpty, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsntEmpty, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsntEmpty, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1); + + // HasAttachmentStatus does not work reliably until the user has opened a + // message to force it through MIME. We need a solution for this (bug 105169) + // but in the meantime, I'm doing the same thing here that we do in the + // offline mail table, as this does not really depend at the moment on + // whether we have downloaded the body for offline use. + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::HasAttachmentStatus, nsMsgSearchOp::Isnt, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::Is, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1); + + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_onlineManualFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + m_onlineManualFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + + return rv; +} +// clang-format on diff --git a/comm/mailnews/search/src/nsMsgLocalSearch.cpp b/comm/mailnews/search/src/nsMsgLocalSearch.cpp new file mode 100644 index 0000000000..e43c0dd9d9 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgLocalSearch.cpp @@ -0,0 +1,919 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Implementation of db search for POP and offline IMAP mail folders + +#include "msgCore.h" +#include "nsIMsgDatabase.h" +#include "nsMsgSearchCore.h" +#include "nsMsgLocalSearch.h" +#include "nsIStreamListener.h" +#include "nsMsgSearchBoolExpression.h" +#include "nsMsgSearchTerm.h" +#include "nsMsgResultElement.h" +#include "nsIDBFolderInfo.h" +#include "nsMsgSearchValue.h" +#include "nsIMsgLocalMailFolder.h" +#include "nsIMsgWindow.h" +#include "nsIMsgHdr.h" +#include "nsIMsgFilterPlugin.h" +#include "nsMsgMessageFlags.h" +#include "nsMsgUtils.h" +#include "nsIMsgFolder.h" + +extern "C" { +extern int MK_MSG_SEARCH_STATUS; +extern int MK_MSG_CANT_SEARCH_IF_NO_SUMMARY; +extern int MK_MSG_SEARCH_HITS_NOT_IN_DB; +} + +//---------------------------------------------------------------------------- +// Class definitions for the boolean expression structure.... +//---------------------------------------------------------------------------- + +nsMsgSearchBoolExpression* nsMsgSearchBoolExpression::AddSearchTerm( + nsMsgSearchBoolExpression* aOrigExpr, nsIMsgSearchTerm* aNewTerm, + char* aEncodingStr) +// appropriately add the search term to the current expression and return a +// pointer to the new expression. The encodingStr is the IMAP/NNTP encoding +// string for newTerm. +{ + return aOrigExpr->leftToRightAddTerm(aNewTerm, aEncodingStr); +} + +nsMsgSearchBoolExpression* nsMsgSearchBoolExpression::AddExpressionTree( + nsMsgSearchBoolExpression* aOrigExpr, + nsMsgSearchBoolExpression* aExpression, bool aBoolOp) { + if (!aOrigExpr->m_term && !aOrigExpr->m_leftChild && + !aOrigExpr->m_rightChild) { + // just use the original expression tree... + // delete the original since we have a new original to use + delete aOrigExpr; + return aExpression; + } + + nsMsgSearchBoolExpression* newExpr = + new nsMsgSearchBoolExpression(aOrigExpr, aExpression, aBoolOp); + return (newExpr) ? newExpr : aOrigExpr; +} + +nsMsgSearchBoolExpression::nsMsgSearchBoolExpression() { + m_term = nullptr; + m_boolOp = nsMsgSearchBooleanOp::BooleanAND; + m_leftChild = nullptr; + m_rightChild = nullptr; +} + +nsMsgSearchBoolExpression::nsMsgSearchBoolExpression(nsIMsgSearchTerm* newTerm, + char* encodingStr) +// we are creating an expression which contains a single search term (newTerm) +// and the search term's IMAP or NNTP encoding value for online search +// expressions AND a boolean evaluation value which is used for offline search +// expressions. +{ + m_term = newTerm; + m_encodingStr = encodingStr; + m_boolOp = nsMsgSearchBooleanOp::BooleanAND; + + // this expression does not contain sub expressions + m_leftChild = nullptr; + m_rightChild = nullptr; +} + +nsMsgSearchBoolExpression::nsMsgSearchBoolExpression( + nsMsgSearchBoolExpression* expr1, nsMsgSearchBoolExpression* expr2, + nsMsgSearchBooleanOperator boolOp) +// we are creating an expression which contains two sub expressions and a +// boolean operator used to combine them. +{ + m_leftChild = expr1; + m_rightChild = expr2; + m_boolOp = boolOp; + + m_term = nullptr; +} + +nsMsgSearchBoolExpression::~nsMsgSearchBoolExpression() { + // we must recursively destroy all sub expressions before we destroy + // ourself.....We leave search terms alone! + delete m_leftChild; + delete m_rightChild; +} + +nsMsgSearchBoolExpression* nsMsgSearchBoolExpression::leftToRightAddTerm( + nsIMsgSearchTerm* newTerm, char* encodingStr) { + // we have a base case where this is the first term being added to the + // expression: + if (!m_term && !m_leftChild && !m_rightChild) { + m_term = newTerm; + m_encodingStr = encodingStr; + return this; + } + + nsMsgSearchBoolExpression* tempExpr = + new nsMsgSearchBoolExpression(newTerm, encodingStr); + if (tempExpr) // make sure creation succeeded + { + bool booleanAnd; + newTerm->GetBooleanAnd(&booleanAnd); + nsMsgSearchBoolExpression* newExpr = + new nsMsgSearchBoolExpression(this, tempExpr, booleanAnd); + if (newExpr) + return newExpr; + else + delete tempExpr; // clean up memory allocation in case of failure + } + return this; // in case we failed to create a new expression, return self +} + +// returns true or false depending on what the current expression evaluates to. +bool nsMsgSearchBoolExpression::OfflineEvaluate(nsIMsgDBHdr* msgToMatch, + const char* defaultCharset, + nsIMsgSearchScopeTerm* scope, + nsIMsgDatabase* db, + const nsACString& headers, + bool Filtering) { + bool result = true; // always default to false positives + bool isAnd; + + if (m_term) // do we contain just a search term? + { + nsMsgSearchOfflineMail::ProcessSearchTerm(msgToMatch, m_term, + defaultCharset, scope, db, + headers, Filtering, &result); + return result; + } + + // otherwise we must recursively determine the value of our sub expressions + + isAnd = (m_boolOp == nsMsgSearchBooleanOp::BooleanAND); + + if (m_leftChild) { + result = m_leftChild->OfflineEvaluate(msgToMatch, defaultCharset, scope, db, + headers, Filtering); + if ((result && !isAnd) || (!result && isAnd)) return result; + } + + // If we got this far, either there was no leftChild (which is impossible) + // or we got (FALSE and OR) or (TRUE and AND) from the first result. That + // means the outcome depends entirely on the rightChild. + if (m_rightChild) + result = m_rightChild->OfflineEvaluate(msgToMatch, defaultCharset, scope, + db, headers, Filtering); + + return result; +} + +// ### Maybe we can get rid of these because of our use of nsString??? +// constants used for online searching with IMAP/NNTP encoded search terms. +// the + 1 is to account for null terminators we add at each stage of assembling +// the expression... +const int sizeOfORTerm = + 6 + 1; // 6 bytes if we are combining two sub expressions with an OR term +const int sizeOfANDTerm = + 1 + 1; // 1 byte if we are combining two sub expressions with an AND term + +int32_t nsMsgSearchBoolExpression::CalcEncodeStrSize() +// recursively examine each sub expression and calculate a final size for the +// entire IMAP/NNTP encoding +{ + if (!m_term && (!m_leftChild || !m_rightChild)) // is the expression empty? + return 0; + if (m_term) // are we a leaf node? + return m_encodingStr.Length(); + if (m_boolOp == nsMsgSearchBooleanOp::BooleanOR) + return sizeOfORTerm + m_leftChild->CalcEncodeStrSize() + + m_rightChild->CalcEncodeStrSize(); + if (m_boolOp == nsMsgSearchBooleanOp::BooleanAND) + return sizeOfANDTerm + m_leftChild->CalcEncodeStrSize() + + m_rightChild->CalcEncodeStrSize(); + return 0; +} + +void nsMsgSearchBoolExpression::GenerateEncodeStr(nsCString* buffer) +// recursively combine sub expressions to form a single IMAP/NNTP encoded string +{ + if ((!m_term && (!m_leftChild || !m_rightChild))) // is expression empty? + return; + + if (m_term) // are we a leaf expression? + { + *buffer += m_encodingStr; + return; + } + + // add encode strings of each sub expression + if (m_boolOp == nsMsgSearchBooleanOp::BooleanOR) { + *buffer += " (OR"; + + m_leftChild->GenerateEncodeStr( + buffer); // insert left expression into the buffer + m_rightChild->GenerateEncodeStr( + buffer); // insert right expression into the buffer + + // HACK ALERT!!! if last returned character in the buffer is now a ' ' then + // we need to remove it because we don't want a ' ' to preceded the closing + // paren in the OR encoding. + uint32_t lastCharPos = buffer->Length() - 1; + if (buffer->CharAt(lastCharPos) == ' ') { + buffer->SetLength(lastCharPos); + } + + *buffer += ')'; + } else if (m_boolOp == nsMsgSearchBooleanOp::BooleanAND) { + m_leftChild->GenerateEncodeStr(buffer); // insert left expression + m_rightChild->GenerateEncodeStr(buffer); + } + return; +} + +//----------------------------------------------------------------------------- +//---------------- Adapter class for searching offline folders ---------------- +//----------------------------------------------------------------------------- + +NS_IMPL_ISUPPORTS_INHERITED(nsMsgSearchOfflineMail, nsMsgSearchAdapter, + nsIUrlListener) + +nsMsgSearchOfflineMail::nsMsgSearchOfflineMail( + nsIMsgSearchScopeTerm* scope, + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList) + : nsMsgSearchAdapter(scope, termList) {} + +nsMsgSearchOfflineMail::~nsMsgSearchOfflineMail() { + // Database should have been closed when the scope term finished. + CleanUpScope(); + NS_ASSERTION(!m_db, "db not closed"); +} + +nsresult nsMsgSearchOfflineMail::ValidateTerms() { + return nsMsgSearchAdapter::ValidateTerms(); +} + +nsresult nsMsgSearchOfflineMail::OpenSummaryFile() { + nsCOMPtr<nsIMsgDatabase> mailDB; + + nsresult err = NS_OK; + // do password protection of local cache thing. +#ifdef DOING_FOLDER_CACHE_PASSWORDS + if (m_scope->m_folder && + m_scope->m_folder->UserNeedsToAuthenticateForFolder(false) && + m_scope->m_folder->GetMaster()->PromptForHostPassword( + m_scope->m_frame->GetContext(), m_scope->m_folder) != 0) { + m_scope->m_frame->StopRunning(); + return SearchError_ScopeDone; + } +#endif + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsCOMPtr<nsIMsgFolder> scopeFolder; + err = m_scope->GetFolder(getter_AddRefs(scopeFolder)); + if (NS_SUCCEEDED(err) && scopeFolder) { + err = scopeFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(m_db)); + } else + return err; // not sure why m_folder wouldn't be set. + + if (NS_SUCCEEDED(err)) return NS_OK; + + if ((err == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) || + (err == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE)) { + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = + do_QueryInterface(scopeFolder, &err); + if (NS_SUCCEEDED(err) && localFolder) { + nsCOMPtr<nsIMsgSearchSession> searchSession; + m_scope->GetSearchSession(getter_AddRefs(searchSession)); + if (searchSession) { + nsCOMPtr<nsIMsgWindow> searchWindow; + + searchSession->GetWindow(getter_AddRefs(searchWindow)); + searchSession->PauseSearch(); + localFolder->ParseFolder(searchWindow, this); + } + } + } else { + NS_ASSERTION(false, "unexpected error opening db"); + } + + return err; +} + +nsresult nsMsgSearchOfflineMail::MatchTermsForFilter( + nsIMsgDBHdr* msgToMatch, nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList, + const char* defaultCharset, nsIMsgSearchScopeTerm* scope, + nsIMsgDatabase* db, const nsACString& headers, + nsMsgSearchBoolExpression** aExpressionTree, bool* pResult) { + return MatchTerms(msgToMatch, termList, defaultCharset, scope, db, headers, + true, aExpressionTree, pResult); +} + +// static method which matches a header against a list of search terms. +nsresult nsMsgSearchOfflineMail::MatchTermsForSearch( + nsIMsgDBHdr* msgToMatch, nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList, + const char* defaultCharset, nsIMsgSearchScopeTerm* scope, + nsIMsgDatabase* db, nsMsgSearchBoolExpression** aExpressionTree, + bool* pResult) { + return MatchTerms(msgToMatch, termList, defaultCharset, scope, db, + EmptyCString(), false, aExpressionTree, pResult); +} + +nsresult nsMsgSearchOfflineMail::ConstructExpressionTree( + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList, uint32_t termCount, + uint32_t& aStartPosInList, nsMsgSearchBoolExpression** aExpressionTree) { + nsMsgSearchBoolExpression* finalExpression = *aExpressionTree; + + if (!finalExpression) finalExpression = new nsMsgSearchBoolExpression(); + + while (aStartPosInList < termCount) { + nsIMsgSearchTerm* pTerm = termList[aStartPosInList]; + NS_ASSERTION(pTerm, "couldn't get term to match"); + + bool beginsGrouping; + bool endsGrouping; + pTerm->GetBeginsGrouping(&beginsGrouping); + pTerm->GetEndsGrouping(&endsGrouping); + + if (beginsGrouping) { + // temporarily turn off the grouping for our recursive call + pTerm->SetBeginsGrouping(false); + nsMsgSearchBoolExpression* innerExpression = + new nsMsgSearchBoolExpression(); + + // the first search term in the grouping is the one that holds the + // operator for how this search term should be joined with the expressions + // to it's left. + bool booleanAnd; + pTerm->GetBooleanAnd(&booleanAnd); + + // now add this expression tree to our overall expression tree... + finalExpression = nsMsgSearchBoolExpression::AddExpressionTree( + finalExpression, innerExpression, booleanAnd); + + // recursively process this inner expression + ConstructExpressionTree(termList, termCount, aStartPosInList, + &finalExpression->m_rightChild); + + // undo our damage + pTerm->SetBeginsGrouping(true); + + } else { + finalExpression = nsMsgSearchBoolExpression::AddSearchTerm( + finalExpression, pTerm, + nullptr); // add the term to the expression tree + + if (endsGrouping) break; + } + + aStartPosInList++; + } // while we still have terms to process in this group + + *aExpressionTree = finalExpression; + + return NS_OK; +} + +nsresult nsMsgSearchOfflineMail::ProcessSearchTerm( + nsIMsgDBHdr* msgToMatch, nsIMsgSearchTerm* aTerm, + const char* defaultCharset, nsIMsgSearchScopeTerm* scope, + nsIMsgDatabase* db, const nsACString& headers, bool Filtering, + bool* pResult) { + nsresult err = NS_OK; + nsCString recipients; + nsCString ccList; + nsCString matchString; + nsCString msgCharset; + const char* charset; + bool charsetOverride = false; /* XXX BUG 68706 */ + uint32_t msgFlags; + bool result; + bool matchAll; + + NS_ENSURE_ARG_POINTER(pResult); + + aTerm->GetMatchAll(&matchAll); + if (matchAll) { + *pResult = true; + return NS_OK; + } + *pResult = false; + + nsMsgSearchAttribValue attrib; + aTerm->GetAttrib(&attrib); + msgToMatch->GetCharset(getter_Copies(msgCharset)); + charset = msgCharset.get(); + if (!charset || !*charset) charset = (const char*)defaultCharset; + msgToMatch->GetFlags(&msgFlags); + + switch (attrib) { + case nsMsgSearchAttrib::Sender: + msgToMatch->GetAuthor(getter_Copies(matchString)); + err = aTerm->MatchRfc822String(matchString, charset, &result); + break; + case nsMsgSearchAttrib::Subject: { + msgToMatch->GetSubject(matchString /* , true */); + if (msgFlags & nsMsgMessageFlags::HasRe) { + // Make sure we pass along the "Re: " part of the subject if this is a + // reply. + nsCString reString; + reString.AssignLiteral("Re: "); + reString.Append(matchString); + err = aTerm->MatchRfc2047String(reString, charset, charsetOverride, + &result); + } else + err = aTerm->MatchRfc2047String(matchString, charset, charsetOverride, + &result); + break; + } + case nsMsgSearchAttrib::ToOrCC: { + bool boolKeepGoing; + aTerm->GetMatchAllBeforeDeciding(&boolKeepGoing); + msgToMatch->GetRecipients(getter_Copies(recipients)); + err = aTerm->MatchRfc822String(recipients, charset, &result); + if (boolKeepGoing == result) { + msgToMatch->GetCcList(getter_Copies(ccList)); + err = aTerm->MatchRfc822String(ccList, charset, &result); + } + break; + } + case nsMsgSearchAttrib::AllAddresses: { + bool boolKeepGoing; + aTerm->GetMatchAllBeforeDeciding(&boolKeepGoing); + msgToMatch->GetRecipients(getter_Copies(recipients)); + err = aTerm->MatchRfc822String(recipients, charset, &result); + if (boolKeepGoing == result) { + msgToMatch->GetCcList(getter_Copies(ccList)); + err = aTerm->MatchRfc822String(ccList, charset, &result); + } + if (boolKeepGoing == result) { + msgToMatch->GetAuthor(getter_Copies(matchString)); + err = aTerm->MatchRfc822String(matchString, charset, &result); + } + if (boolKeepGoing == result) { + nsCString bccList; + msgToMatch->GetBccList(getter_Copies(bccList)); + err = aTerm->MatchRfc822String(bccList, charset, &result); + } + break; + } + case nsMsgSearchAttrib::Body: { + uint64_t messageOffset; + uint32_t lineCount; + msgToMatch->GetMessageOffset(&messageOffset); + msgToMatch->GetLineCount(&lineCount); + err = aTerm->MatchBody(scope, messageOffset, lineCount, charset, + msgToMatch, db, &result); + break; + } + case nsMsgSearchAttrib::Date: { + PRTime date; + msgToMatch->GetDate(&date); + err = aTerm->MatchDate(date, &result); + + break; + } + case nsMsgSearchAttrib::HasAttachmentStatus: + case nsMsgSearchAttrib::MsgStatus: + err = aTerm->MatchStatus(msgFlags, &result); + break; + case nsMsgSearchAttrib::Priority: { + nsMsgPriorityValue msgPriority; + msgToMatch->GetPriority(&msgPriority); + err = aTerm->MatchPriority(msgPriority, &result); + break; + } + case nsMsgSearchAttrib::Size: { + uint32_t messageSize; + msgToMatch->GetMessageSize(&messageSize); + err = aTerm->MatchSize(messageSize, &result); + break; + } + case nsMsgSearchAttrib::To: + msgToMatch->GetRecipients(getter_Copies(recipients)); + err = aTerm->MatchRfc822String(recipients, charset, &result); + break; + case nsMsgSearchAttrib::CC: + msgToMatch->GetCcList(getter_Copies(ccList)); + err = aTerm->MatchRfc822String(ccList, charset, &result); + break; + case nsMsgSearchAttrib::AgeInDays: { + PRTime date; + msgToMatch->GetDate(&date); + err = aTerm->MatchAge(date, &result); + break; + } + case nsMsgSearchAttrib::Keywords: { + nsCString keywords; + msgToMatch->GetStringProperty("keywords", keywords); + err = aTerm->MatchKeyword(keywords, &result); + break; + } + case nsMsgSearchAttrib::JunkStatus: { + nsCString junkScoreStr; + msgToMatch->GetStringProperty("junkscore", junkScoreStr); + err = aTerm->MatchJunkStatus(junkScoreStr.get(), &result); + break; + } + case nsMsgSearchAttrib::JunkPercent: { + // When the junk status is set by the plugin, use junkpercent (if + // available) Otherwise, use the limits (0 or 100) depending on the + // junkscore. + uint32_t junkPercent; + nsresult rv; + nsCString junkScoreOriginStr; + nsCString junkPercentStr; + msgToMatch->GetStringProperty("junkscoreorigin", junkScoreOriginStr); + msgToMatch->GetStringProperty("junkpercent", junkPercentStr); + if (junkScoreOriginStr.EqualsLiteral("plugin") && + !junkPercentStr.IsEmpty()) { + junkPercent = junkPercentStr.ToInteger(&rv); + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsCString junkScoreStr; + msgToMatch->GetStringProperty("junkscore", junkScoreStr); + // When junk status is not set (uncertain) we'll set the value to ham. + if (junkScoreStr.IsEmpty()) + junkPercent = nsIJunkMailPlugin::IS_HAM_SCORE; + else { + junkPercent = junkScoreStr.ToInteger(&rv); + NS_ENSURE_SUCCESS(rv, rv); + } + } + err = aTerm->MatchJunkPercent(junkPercent, &result); + break; + } + case nsMsgSearchAttrib::JunkScoreOrigin: { + nsCString junkScoreOriginStr; + msgToMatch->GetStringProperty("junkscoreorigin", junkScoreOriginStr); + err = aTerm->MatchJunkScoreOrigin(junkScoreOriginStr.get(), &result); + break; + } + case nsMsgSearchAttrib::HdrProperty: { + err = aTerm->MatchHdrProperty(msgToMatch, &result); + break; + } + case nsMsgSearchAttrib::Uint32HdrProperty: { + err = aTerm->MatchUint32HdrProperty(msgToMatch, &result); + break; + } + case nsMsgSearchAttrib::Custom: { + err = aTerm->MatchCustom(msgToMatch, &result); + break; + } + case nsMsgSearchAttrib::FolderFlag: { + err = aTerm->MatchFolderFlag(msgToMatch, &result); + break; + } + default: + // XXX todo + // for the temporary return receipts filters, we use a custom header for + // Content-Type but unlike the other custom headers, this one doesn't show + // up in the search / filter UI. we set the attrib to be + // nsMsgSearchAttrib::OtherHeader, where as for user defined custom + // headers start at nsMsgSearchAttrib::OtherHeader + 1 Not sure if there + // is a better way to do this yet. Maybe reserve the last custom header + // for ::Content-Type? But if we do, make sure that change doesn't cause + // nsMsgFilter::GetTerm() to change, and start making us ask IMAP servers + // for the Content-Type header on all messages. + if (attrib >= nsMsgSearchAttrib::OtherHeader && + attrib < nsMsgSearchAttrib::kNumMsgSearchAttributes) { + uint32_t lineCount; + msgToMatch->GetLineCount(&lineCount); + uint64_t messageOffset; + msgToMatch->GetMessageOffset(&messageOffset); + err = aTerm->MatchArbitraryHeader(scope, lineCount, charset, + charsetOverride, msgToMatch, db, + headers, Filtering, &result); + } else { + err = NS_ERROR_INVALID_ARG; // ### was SearchError_InvalidAttribute + result = false; + } + } + + *pResult = result; + return err; +} + +nsresult nsMsgSearchOfflineMail::MatchTerms( + nsIMsgDBHdr* msgToMatch, nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList, + const char* defaultCharset, nsIMsgSearchScopeTerm* scope, + nsIMsgDatabase* db, const nsACString& headers, bool Filtering, + nsMsgSearchBoolExpression** aExpressionTree, bool* pResult) { + NS_ENSURE_ARG(aExpressionTree); + nsresult err; + + if (!*aExpressionTree) { + uint32_t initialPos = 0; + uint32_t count = termList.Length(); + err = ConstructExpressionTree(termList, count, initialPos, aExpressionTree); + if (NS_FAILED(err)) return err; + } + + // evaluate the expression tree and return the result + *pResult = (*aExpressionTree) + ? (*aExpressionTree) + ->OfflineEvaluate(msgToMatch, defaultCharset, scope, db, + headers, Filtering) + : true; // vacuously true... + + return NS_OK; +} + +nsresult nsMsgSearchOfflineMail::Search(bool* aDone) { + nsresult err = NS_OK; + + NS_ENSURE_ARG(aDone); + nsresult dbErr = NS_OK; + nsMsgSearchBoolExpression* expressionTree = nullptr; + + const uint32_t kTimeSliceInMS = 200; + + *aDone = false; + // Try to open the DB lazily. This will set up a parser if one is required + if (!m_db) err = OpenSummaryFile(); + if (!m_db) // must be reparsing. + return err; + + // Reparsing is unnecessary or completed + if (NS_SUCCEEDED(err)) { + if (!m_listContext) + dbErr = m_db->ReverseEnumerateMessages(getter_AddRefs(m_listContext)); + if (NS_SUCCEEDED(dbErr) && m_listContext) { + PRIntervalTime startTime = PR_IntervalNow(); + while (!*aDone) // we'll break out of the loop after kTimeSliceInMS + // milliseconds + { + nsCOMPtr<nsIMsgDBHdr> msgDBHdr; + dbErr = m_listContext->GetNext(getter_AddRefs(msgDBHdr)); + if (NS_FAILED(dbErr)) + *aDone = true; // ###phil dbErr is dropped on the floor. just note + // that we did have an error so we'll clean up later + else { + bool match = false; + nsAutoString nullCharset, folderCharset; + GetSearchCharsets(nullCharset, folderCharset); + NS_ConvertUTF16toUTF8 charset(folderCharset); + // Is this message a hit? + err = MatchTermsForSearch(msgDBHdr, m_searchTerms, charset.get(), + m_scope, m_db, &expressionTree, &match); + // Add search hits to the results list + if (NS_SUCCEEDED(err) && match) { + AddResultElement(msgDBHdr); + } + PRIntervalTime elapsedTime = PR_IntervalNow() - startTime; + // check if more than kTimeSliceInMS milliseconds have elapsed in this + // time slice started + if (PR_IntervalToMilliseconds(elapsedTime) > kTimeSliceInMS) break; + } + } + } + } else + *aDone = true; // we couldn't open up the DB. This is an unrecoverable + // error so mark the scope as done. + + delete expressionTree; + + // in the past an error here would cause an "infinite" search because the url + // would continue to run... i.e. if we couldn't open the database, it returns + // an error code but the caller of this function says, oh, we did not finish + // so continue...what we really want is to treat this current scope as done + if (*aDone) CleanUpScope(); // Do clean up for end-of-scope processing + return err; +} + +void nsMsgSearchOfflineMail::CleanUpScope() { + // Let go of the DB when we're done with it so we don't kill the db cache + if (m_db) { + m_listContext = nullptr; + m_db->Close(false); + } + m_db = nullptr; + + if (m_scope) m_scope->CloseInputStream(); +} + +NS_IMETHODIMP nsMsgSearchOfflineMail::AddResultElement(nsIMsgDBHdr* pHeaders) { + nsresult err = NS_OK; + + nsCOMPtr<nsIMsgSearchSession> searchSession; + m_scope->GetSearchSession(getter_AddRefs(searchSession)); + if (searchSession) { + nsCOMPtr<nsIMsgFolder> scopeFolder; + err = m_scope->GetFolder(getter_AddRefs(scopeFolder)); + searchSession->AddSearchHit(pHeaders, scopeFolder); + } + return err; +} + +NS_IMETHODIMP +nsMsgSearchOfflineMail::Abort() { + // Let go of the DB when we're done with it so we don't kill the db cache + if (m_db) m_db->Close(true /* commit in case we downloaded new headers */); + m_db = nullptr; + return nsMsgSearchAdapter::Abort(); +} + +/* void OnStartRunningUrl (in nsIURI url); */ +NS_IMETHODIMP nsMsgSearchOfflineMail::OnStartRunningUrl(nsIURI* url) { + return NS_OK; +} + +/* void OnStopRunningUrl (in nsIURI url, in nsresult aExitCode); */ +NS_IMETHODIMP nsMsgSearchOfflineMail::OnStopRunningUrl(nsIURI* url, + nsresult aExitCode) { + nsCOMPtr<nsIMsgSearchSession> searchSession; + if (m_scope) m_scope->GetSearchSession(getter_AddRefs(searchSession)); + if (searchSession) searchSession->ResumeSearch(); + + return NS_OK; +} + +nsMsgSearchOfflineNews::nsMsgSearchOfflineNews( + nsIMsgSearchScopeTerm* scope, + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList) + : nsMsgSearchOfflineMail(scope, termList) {} + +nsMsgSearchOfflineNews::~nsMsgSearchOfflineNews() {} + +nsresult nsMsgSearchOfflineNews::OpenSummaryFile() { + nsresult err = NS_OK; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsCOMPtr<nsIMsgFolder> scopeFolder; + err = m_scope->GetFolder(getter_AddRefs(scopeFolder)); + // code here used to check if offline store existed, which breaks offline + // news. + if (NS_SUCCEEDED(err) && scopeFolder) + err = scopeFolder->GetMsgDatabase(getter_AddRefs(m_db)); + return err; +} + +nsresult nsMsgSearchOfflineNews::ValidateTerms() { + return nsMsgSearchOfflineMail::ValidateTerms(); +} + +// local helper functions to set subsets of the validity table +// clang-format off +nsresult SetJunk(nsIMsgSearchValidityTable *aTable) { + NS_ENSURE_ARG_POINTER(aTable); + + aTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1); + aTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Is, 1); + aTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1); + aTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::Isnt, 1); + aTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsEmpty, 1); + aTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsEmpty, 1); + aTable->SetAvailable(nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsntEmpty, 1); + aTable->SetEnabled (nsMsgSearchAttrib::JunkStatus, nsMsgSearchOp::IsntEmpty, 1); + + aTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1); + aTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsGreaterThan, 1); + aTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1); + aTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::IsLessThan, 1); + aTable->SetAvailable(nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1); + aTable->SetEnabled (nsMsgSearchAttrib::JunkPercent, nsMsgSearchOp::Is, 1); + + aTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1); + aTable->SetEnabled (nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Is, 1); + aTable->SetAvailable(nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1); + aTable->SetEnabled (nsMsgSearchAttrib::JunkScoreOrigin, nsMsgSearchOp::Isnt, 1); + + return NS_OK; +} + +nsresult SetBody(nsIMsgSearchValidityTable* aTable) { + NS_ENSURE_ARG_POINTER(aTable); + + aTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Contains, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::DoesntContain, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Is, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Body, nsMsgSearchOp::Isnt, 1); + + return NS_OK; +} + +// set the base validity table values for local news +nsresult SetLocalNews(nsIMsgSearchValidityTable* aTable) { + NS_ENSURE_ARG_POINTER(aTable); + + aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1); + + aTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + + aTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + + aTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1); + aTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsGreaterThan, 1); + aTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1); + aTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::IsLessThan, 1); + aTable->SetAvailable(nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1); + aTable->SetEnabled (nsMsgSearchAttrib::AgeInDays, nsMsgSearchOp::Is, 1); + + aTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1); + aTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Is, 1); + aTable->SetAvailable(nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1); + aTable->SetEnabled (nsMsgSearchAttrib::MsgStatus, nsMsgSearchOp::Isnt, 1); + + aTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Contains, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::DoesntContain, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Is, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::Isnt, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsEmpty, 1); + aTable->SetAvailable(nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1); + aTable->SetEnabled (nsMsgSearchAttrib::Keywords, nsMsgSearchOp::IsntEmpty, 1); + + aTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + aTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + aTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1); + aTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1); + aTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + aTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + aTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1); + aTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1); + aTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + aTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + aTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + aTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + return NS_OK; +} +// clang-format on + +nsresult nsMsgSearchValidityManager::InitLocalNewsTable() { + NS_ASSERTION(nullptr == m_localNewsTable, + "already have local news validity table"); + nsresult rv = NewTable(getter_AddRefs(m_localNewsTable)); + NS_ENSURE_SUCCESS(rv, rv); + return SetLocalNews(m_localNewsTable); +} + +nsresult nsMsgSearchValidityManager::InitLocalNewsBodyTable() { + NS_ASSERTION(nullptr == m_localNewsBodyTable, + "already have local news+body validity table"); + nsresult rv = NewTable(getter_AddRefs(m_localNewsBodyTable)); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetLocalNews(m_localNewsBodyTable); + NS_ENSURE_SUCCESS(rv, rv); + return SetBody(m_localNewsBodyTable); +} + +nsresult nsMsgSearchValidityManager::InitLocalNewsJunkTable() { + NS_ASSERTION(nullptr == m_localNewsJunkTable, + "already have local news+junk validity table"); + nsresult rv = NewTable(getter_AddRefs(m_localNewsJunkTable)); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetLocalNews(m_localNewsJunkTable); + NS_ENSURE_SUCCESS(rv, rv); + return SetJunk(m_localNewsJunkTable); +} + +nsresult nsMsgSearchValidityManager::InitLocalNewsJunkBodyTable() { + NS_ASSERTION(nullptr == m_localNewsJunkBodyTable, + "already have local news+junk+body validity table"); + nsresult rv = NewTable(getter_AddRefs(m_localNewsJunkBodyTable)); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetLocalNews(m_localNewsJunkBodyTable); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetJunk(m_localNewsJunkBodyTable); + NS_ENSURE_SUCCESS(rv, rv); + return SetBody(m_localNewsJunkBodyTable); +} diff --git a/comm/mailnews/search/src/nsMsgLocalSearch.h b/comm/mailnews/search/src/nsMsgLocalSearch.h new file mode 100644 index 0000000000..4deb5b4065 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgLocalSearch.h @@ -0,0 +1,90 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgLocalSearch_H +#define _nsMsgLocalSearch_H + +// inherit interface here +#include "mozilla/Attributes.h" +#include "nsIMsgSearchAdapter.h" +#include "nsIUrlListener.h" + +// inherit base implementation +#include "nsMsgSearchAdapter.h" + +class nsIMsgDBHdr; +class nsIMsgSearchScopeTerm; +class nsIMsgFolder; +class nsMsgSearchBoolExpression; + +class nsMsgSearchOfflineMail : public nsMsgSearchAdapter, + public nsIUrlListener { + public: + nsMsgSearchOfflineMail(nsIMsgSearchScopeTerm*, + nsTArray<RefPtr<nsIMsgSearchTerm>> const&); + + NS_DECL_ISUPPORTS_INHERITED + + NS_DECL_NSIURLLISTENER + + NS_IMETHOD ValidateTerms() override; + NS_IMETHOD Search(bool* aDone) override; + NS_IMETHOD Abort() override; + NS_IMETHOD AddResultElement(nsIMsgDBHdr*) override; + + static nsresult MatchTermsForFilter( + nsIMsgDBHdr* msgToMatch, + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList, + const char* defaultCharset, nsIMsgSearchScopeTerm* scope, + nsIMsgDatabase* db, const nsACString& headers, + nsMsgSearchBoolExpression** aExpressionTree, bool* pResult); + + static nsresult MatchTermsForSearch( + nsIMsgDBHdr* msgTomatch, + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList, + const char* defaultCharset, nsIMsgSearchScopeTerm* scope, + nsIMsgDatabase* db, nsMsgSearchBoolExpression** aExpressionTree, + bool* pResult); + + virtual nsresult OpenSummaryFile(); + + static nsresult ProcessSearchTerm(nsIMsgDBHdr* msgToMatch, + nsIMsgSearchTerm* aTerm, + const char* defaultCharset, + nsIMsgSearchScopeTerm* scope, + nsIMsgDatabase* db, + const nsACString& headers, bool Filtering, + bool* pResult); + + protected: + virtual ~nsMsgSearchOfflineMail(); + static nsresult MatchTerms(nsIMsgDBHdr* msgToMatch, + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList, + const char* defaultCharset, + nsIMsgSearchScopeTerm* scope, nsIMsgDatabase* db, + const nsACString& headers, bool ForFilters, + nsMsgSearchBoolExpression** aExpressionTree, + bool* pResult); + + static nsresult ConstructExpressionTree( + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList, uint32_t termCount, + uint32_t& aStartPosInList, nsMsgSearchBoolExpression** aExpressionTree); + + nsCOMPtr<nsIMsgDatabase> m_db; + nsCOMPtr<nsIMsgEnumerator> m_listContext; + void CleanUpScope(); +}; + +class nsMsgSearchOfflineNews : public nsMsgSearchOfflineMail { + public: + nsMsgSearchOfflineNews(nsIMsgSearchScopeTerm*, + nsTArray<RefPtr<nsIMsgSearchTerm>> const&); + virtual ~nsMsgSearchOfflineNews(); + NS_IMETHOD ValidateTerms() override; + + virtual nsresult OpenSummaryFile() override; +}; + +#endif diff --git a/comm/mailnews/search/src/nsMsgSearchAdapter.cpp b/comm/mailnews/search/src/nsMsgSearchAdapter.cpp new file mode 100644 index 0000000000..0a2fa5d2ee --- /dev/null +++ b/comm/mailnews/search/src/nsMsgSearchAdapter.cpp @@ -0,0 +1,1109 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsTextFormatter.h" +#include "nsMsgSearchCore.h" +#include "nsMsgSearchAdapter.h" +#include "nsMsgSearchScopeTerm.h" +#include "nsMsgI18N.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIPrefLocalizedString.h" +#include "nsMsgSearchTerm.h" +#include "nsMsgSearchBoolExpression.h" +#include "nsIIOService.h" +#include "nsNetCID.h" +#include "prprf.h" +#include "mozilla/UniquePtr.h" +#include "prmem.h" +#include "MailNewsTypes.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsMemory.h" +#include "nsMsgMessageFlags.h" +#include "mozilla/Attributes.h" +#include "nsIMsgNewsFolder.h" + +// This stuff lives in the base class because the IMAP search syntax +// is used by the Dredd SEARCH command as well as IMAP itself + +// km - the NOT and HEADER strings are not encoded with a trailing +// <space> because they always precede a mnemonic that has a +// preceding <space> and double <space> characters cause some +// imap servers to return an error. +const char* nsMsgSearchAdapter::m_kImapBefore = " SENTBEFORE "; +const char* nsMsgSearchAdapter::m_kImapBody = " BODY "; +const char* nsMsgSearchAdapter::m_kImapCC = " CC "; +const char* nsMsgSearchAdapter::m_kImapFrom = " FROM "; +const char* nsMsgSearchAdapter::m_kImapNot = " NOT"; +const char* nsMsgSearchAdapter::m_kImapUnDeleted = " UNDELETED"; +const char* nsMsgSearchAdapter::m_kImapOr = " OR"; +const char* nsMsgSearchAdapter::m_kImapSince = " SENTSINCE "; +const char* nsMsgSearchAdapter::m_kImapSubject = " SUBJECT "; +const char* nsMsgSearchAdapter::m_kImapTo = " TO "; +const char* nsMsgSearchAdapter::m_kImapHeader = " HEADER"; +const char* nsMsgSearchAdapter::m_kImapAnyText = " TEXT "; +const char* nsMsgSearchAdapter::m_kImapKeyword = " KEYWORD "; +const char* nsMsgSearchAdapter::m_kNntpKeywords = " KEYWORDS "; // ggrrrr... +const char* nsMsgSearchAdapter::m_kImapSentOn = " SENTON "; +const char* nsMsgSearchAdapter::m_kImapSeen = " SEEN "; +const char* nsMsgSearchAdapter::m_kImapAnswered = " ANSWERED "; +const char* nsMsgSearchAdapter::m_kImapNotSeen = " UNSEEN "; +const char* nsMsgSearchAdapter::m_kImapNotAnswered = " UNANSWERED "; +const char* nsMsgSearchAdapter::m_kImapCharset = " CHARSET "; +const char* nsMsgSearchAdapter::m_kImapSizeSmaller = " SMALLER "; +const char* nsMsgSearchAdapter::m_kImapSizeLarger = " LARGER "; +const char* nsMsgSearchAdapter::m_kImapNew = " NEW "; +const char* nsMsgSearchAdapter::m_kImapNotNew = " OLD SEEN "; +const char* nsMsgSearchAdapter::m_kImapFlagged = " FLAGGED "; +const char* nsMsgSearchAdapter::m_kImapNotFlagged = " UNFLAGGED "; + +#define PREF_CUSTOM_HEADERS "mailnews.customHeaders" + +NS_IMETHODIMP nsMsgSearchAdapter::FindTargetFolder(const nsMsgResultElement*, + nsIMsgFolder**) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgSearchAdapter::ModifyResultElement(nsMsgResultElement*, + nsMsgSearchValue*) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgSearchAdapter::OpenResultElement(nsMsgResultElement*) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMPL_ISUPPORTS(nsMsgSearchAdapter, nsIMsgSearchAdapter) + +nsMsgSearchAdapter::nsMsgSearchAdapter( + nsIMsgSearchScopeTerm* scope, + nsTArray<RefPtr<nsIMsgSearchTerm>> const& searchTerms) + : m_scope(scope), m_searchTerms(searchTerms.Clone()) {} + +nsMsgSearchAdapter::~nsMsgSearchAdapter() {} + +NS_IMETHODIMP nsMsgSearchAdapter::ClearScope() { + if (m_scope) { + m_scope->CloseInputStream(); + m_scope = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchAdapter::ValidateTerms() { + // all this used to do is check if the object had been deleted - we can skip + // that. + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchAdapter::Abort() { return NS_ERROR_NOT_IMPLEMENTED; } +NS_IMETHODIMP nsMsgSearchAdapter::Search(bool* aDone) { return NS_OK; } + +NS_IMETHODIMP nsMsgSearchAdapter::SendUrl() { return NS_OK; } + +/* void CurrentUrlDone (in nsresult exitCode); */ +NS_IMETHODIMP nsMsgSearchAdapter::CurrentUrlDone(nsresult exitCode) { + // base implementation doesn't need to do anything. + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchAdapter::GetEncoding(char** encoding) { return NS_OK; } + +NS_IMETHODIMP nsMsgSearchAdapter::AddResultElement(nsIMsgDBHdr* pHeaders) { + NS_ASSERTION(false, "shouldn't call this base class impl"); + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsMsgSearchAdapter::AddHit(nsMsgKey key) { + NS_ASSERTION(false, "shouldn't call this base class impl"); + return NS_ERROR_FAILURE; +} + +char* nsMsgSearchAdapter::GetImapCharsetParam(const char16_t* destCharset) { + char* result = nullptr; + + // Specify a character set unless we happen to be US-ASCII. + if (NS_strcmp(destCharset, u"us-ascii")) + result = PR_smprintf("%s%s", nsMsgSearchAdapter::m_kImapCharset, + NS_ConvertUTF16toUTF8(destCharset).get()); + + return result; +} + +/* + 09/21/2000 - taka@netscape.com + This method is bogus. Escape must be done against char * not char16_t * + should be rewritten later. + for now, just duplicate the string. +*/ +char16_t* nsMsgSearchAdapter::EscapeSearchUrl(const char16_t* nntpCommand) { + return nntpCommand ? NS_xstrdup(nntpCommand) : nullptr; +} + +/* + 09/21/2000 - taka@netscape.com + This method is bogus. Escape must be done against char * not char16_t * + should be rewritten later. + for now, just duplicate the string. +*/ +char16_t* nsMsgSearchAdapter::EscapeImapSearchProtocol( + const char16_t* imapCommand) { + return imapCommand ? NS_xstrdup(imapCommand) : nullptr; +} + +/* + 09/21/2000 - taka@netscape.com + This method is bogus. Escape must be done against char * not char16_t * + should be rewritten later. + for now, just duplicate the string. +*/ +char16_t* nsMsgSearchAdapter::EscapeQuoteImapSearchProtocol( + const char16_t* imapCommand) { + return imapCommand ? NS_xstrdup(imapCommand) : nullptr; +} + +char* nsMsgSearchAdapter::UnEscapeSearchUrl(const char* commandSpecificData) { + char* result = (char*)PR_Malloc(strlen(commandSpecificData) + 1); + if (result) { + char* resultPtr = result; + while (1) { + char ch = *commandSpecificData++; + if (!ch) break; + if (ch == '\\') { + char scratchBuf[3]; + scratchBuf[0] = (char)*commandSpecificData++; + scratchBuf[1] = (char)*commandSpecificData++; + scratchBuf[2] = '\0'; + unsigned int accum = 0; + sscanf(scratchBuf, "%X", &accum); + *resultPtr++ = (char)accum; + } else + *resultPtr++ = ch; + } + *resultPtr = '\0'; + } + return result; +} + +nsresult nsMsgSearchAdapter::GetSearchCharsets(nsAString& srcCharset, + nsAString& dstCharset) { + nsresult rv; + bool forceAsciiSearch = false; + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (NS_SUCCEEDED(rv)) { + prefs->GetBoolPref("mailnews.force_ascii_search", &forceAsciiSearch); + } + + srcCharset = m_defaultCharset; + dstCharset.Assign(srcCharset); + + if (m_scope) { + nsCOMPtr<nsIMsgFolder> folder; + rv = m_scope->GetFolder(getter_AddRefs(folder)); + if (NS_SUCCEEDED(rv) && folder) { + nsCOMPtr<nsIMsgNewsFolder> newsfolder(do_QueryInterface(folder)); + if (newsfolder) { + nsCString folderCharset; + rv = newsfolder->GetCharset(folderCharset); + if (NS_SUCCEEDED(rv)) + dstCharset.Assign(NS_ConvertASCIItoUTF16(folderCharset)); + } + } + } + + if (forceAsciiSearch) { + // Special cases to use in order to force US-ASCII searching with Latin1 + // or MacRoman text. Eurgh. This only has to happen because IMAP + // and Dredd servers currently (4/23/97) only support US-ASCII. + // + // If the dest csid is ISO Latin 1 or MacRoman, attempt to convert the + // source text to US-ASCII. (Not for now.) + // if ((dst_csid == CS_LATIN1) || (dst_csid == CS_MAC_ROMAN)) + dstCharset.AssignLiteral("us-ascii"); + } + + return NS_OK; +} + +nsresult nsMsgSearchAdapter::EncodeImapTerm(nsIMsgSearchTerm* term, + bool reallyDredd, + const char16_t* srcCharset, + const char16_t* destCharset, + char** ppOutTerm) { + NS_ENSURE_ARG_POINTER(term); + NS_ENSURE_ARG_POINTER(ppOutTerm); + + nsresult err = NS_OK; + bool useNot = false; + bool useQuotes = false; + bool ignoreValue = false; + nsAutoCString arbitraryHeader; + const char* whichMnemonic = nullptr; + const char* orHeaderMnemonic = nullptr; + + *ppOutTerm = nullptr; + + nsCOMPtr<nsIMsgSearchValue> searchValue; + nsresult rv = term->GetValue(getter_AddRefs(searchValue)); + + NS_ENSURE_SUCCESS(rv, rv); + + nsMsgSearchOpValue op; + term->GetOp(&op); + + if (op == nsMsgSearchOp::DoesntContain || op == nsMsgSearchOp::Isnt) + useNot = true; + + nsMsgSearchAttribValue attrib; + term->GetAttrib(&attrib); + + switch (attrib) { + case nsMsgSearchAttrib::ToOrCC: + orHeaderMnemonic = m_kImapCC; + // fall through to case nsMsgSearchAttrib::To: + [[fallthrough]]; + case nsMsgSearchAttrib::To: + whichMnemonic = m_kImapTo; + break; + case nsMsgSearchAttrib::CC: + whichMnemonic = m_kImapCC; + break; + case nsMsgSearchAttrib::Sender: + whichMnemonic = m_kImapFrom; + break; + case nsMsgSearchAttrib::Subject: + whichMnemonic = m_kImapSubject; + break; + case nsMsgSearchAttrib::Body: + whichMnemonic = m_kImapBody; + break; + case nsMsgSearchAttrib::AgeInDays: // added for searching online for age in + // days... + // for AgeInDays, we are actually going to perform a search by date, so + // convert the operations for age to the IMAP mnemonics that we would use + // for date! + { + // If we have a future date, the > and < are reversed. + // e.g. ageInDays > 2 means more than 2 days old ("date before X") + // whereas + // ageInDays > -2 should be more than 2 days in the future ("date + // after X") + int32_t ageInDays; + searchValue->GetAge(&ageInDays); + bool dateInFuture = (ageInDays < 0); + switch (op) { + case nsMsgSearchOp::IsGreaterThan: + whichMnemonic = (!dateInFuture) ? m_kImapBefore : m_kImapSince; + break; + case nsMsgSearchOp::IsLessThan: + whichMnemonic = (!dateInFuture) ? m_kImapSince : m_kImapBefore; + break; + case nsMsgSearchOp::Is: + whichMnemonic = m_kImapSentOn; + break; + default: + NS_ASSERTION(false, "invalid search operator"); + return NS_ERROR_INVALID_ARG; + } + } + break; + case nsMsgSearchAttrib::Size: + switch (op) { + case nsMsgSearchOp::IsGreaterThan: + whichMnemonic = m_kImapSizeLarger; + break; + case nsMsgSearchOp::IsLessThan: + whichMnemonic = m_kImapSizeSmaller; + break; + default: + NS_ASSERTION(false, "invalid search operator"); + return NS_ERROR_INVALID_ARG; + } + break; + case nsMsgSearchAttrib::Date: + switch (op) { + case nsMsgSearchOp::IsBefore: + whichMnemonic = m_kImapBefore; + break; + case nsMsgSearchOp::IsAfter: + whichMnemonic = m_kImapSince; + break; + case nsMsgSearchOp::Isnt: /* we've already added the "Not" so just + process it like it was a date is search */ + case nsMsgSearchOp::Is: + whichMnemonic = m_kImapSentOn; + break; + default: + NS_ASSERTION(false, "invalid search operator"); + return NS_ERROR_INVALID_ARG; + } + break; + case nsMsgSearchAttrib::AnyText: + whichMnemonic = m_kImapAnyText; + break; + case nsMsgSearchAttrib::Keywords: + whichMnemonic = m_kImapKeyword; + break; + case nsMsgSearchAttrib::MsgStatus: + useNot = false; // bizarrely, NOT SEEN is wrong, but UNSEEN is right. + ignoreValue = true; // the mnemonic is all we need + uint32_t status; + searchValue->GetStatus(&status); + + switch (status) { + case nsMsgMessageFlags::Read: + whichMnemonic = + op == nsMsgSearchOp::Is ? m_kImapSeen : m_kImapNotSeen; + break; + case nsMsgMessageFlags::Replied: + whichMnemonic = + op == nsMsgSearchOp::Is ? m_kImapAnswered : m_kImapNotAnswered; + break; + case nsMsgMessageFlags::New: + whichMnemonic = op == nsMsgSearchOp::Is ? m_kImapNew : m_kImapNotNew; + break; + case nsMsgMessageFlags::Marked: + whichMnemonic = + op == nsMsgSearchOp::Is ? m_kImapFlagged : m_kImapNotFlagged; + break; + default: + NS_ASSERTION(false, "invalid search operator"); + return NS_ERROR_INVALID_ARG; + } + break; + default: + if (attrib > nsMsgSearchAttrib::OtherHeader && + attrib < nsMsgSearchAttrib::kNumMsgSearchAttributes) { + nsCString arbitraryHeaderTerm; + term->GetArbitraryHeader(arbitraryHeaderTerm); + if (!arbitraryHeaderTerm.IsEmpty()) { + arbitraryHeader.AssignLiteral(" \""); + arbitraryHeader.Append(arbitraryHeaderTerm); + arbitraryHeader.AppendLiteral("\" "); + whichMnemonic = arbitraryHeader.get(); + } else + return NS_ERROR_FAILURE; + } else { + NS_ASSERTION(false, "invalid search operator"); + return NS_ERROR_INVALID_ARG; + } + } + + char* value = nullptr; + char dateBuf[100]; + dateBuf[0] = '\0'; + + bool valueWasAllocated = false; + if (attrib == nsMsgSearchAttrib::Date) { + // note that there used to be code here that encoded an RFC822 date for imap + // searches. The IMAP RFC 2060 is misleading to the point that it looks like + // it requires an RFC822 date but really it expects dd-mmm-yyyy, like dredd, + // and refers to the RFC822 date only in that the dd-mmm-yyyy date will + // match the RFC822 date within the message. + + PRTime adjustedDate; + searchValue->GetDate(&adjustedDate); + if (whichMnemonic == m_kImapSince) { + // it looks like the IMAP server searches on Since includes the date in + // question... our UI presents Is, IsGreater and IsLessThan. For the + // IsGreater case (m_kImapSince) we need to adjust the date so we get + // greater than and not greater than or equal to which is what the IMAP + // server wants to search on won't work on Mac. + adjustedDate += PR_USEC_PER_DAY; + } + + PRExplodedTime exploded; + PR_ExplodeTime(adjustedDate, PR_LocalTimeParameters, &exploded); + PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%d-%b-%Y", &exploded); + // strftime (dateBuf, sizeof(dateBuf), "%d-%b-%Y", localtime (/* + // &term->m_value.u.date */ &adjustedDate)); + value = dateBuf; + } else { + if (attrib == nsMsgSearchAttrib::AgeInDays) { + // okay, take the current date, subtract off the age in days, then do an + // appropriate Date search on the resulting day. + int32_t ageInDays; + + searchValue->GetAge(&ageInDays); + + PRTime now = PR_Now(); + PRTime matchDay = now - ageInDays * PR_USEC_PER_DAY; + + PRExplodedTime exploded; + PR_ExplodeTime(matchDay, PR_LocalTimeParameters, &exploded); + PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%d-%b-%Y", &exploded); + // strftime (dateBuf, sizeof(dateBuf), "%d-%b-%Y", localtime + // (&matchDay)); + value = dateBuf; + } else if (attrib == nsMsgSearchAttrib::Size) { + uint32_t sizeValue; + nsAutoCString searchTermValue; + searchValue->GetSize(&sizeValue); + + // Multiply by 1024 to get into kb resolution + sizeValue *= 1024; + + // Ensure that greater than is really greater than + // in kb resolution. + if (op == nsMsgSearchOp::IsGreaterThan) sizeValue += 1024; + + searchTermValue.AppendInt(sizeValue); + + value = ToNewCString(searchTermValue); + valueWasAllocated = true; + } else + + if (IS_STRING_ATTRIBUTE(attrib)) { + char16_t* + convertedValue; // = reallyDredd ? MSG_EscapeSearchUrl + // (term->m_value.u.string) : + // msg_EscapeImapSearchProtocol(term->m_value.u.string); + nsString searchTermValue; + searchValue->GetStr(searchTermValue); + // Ugly switch for Korean mail/news charsets. + // We want to do this here because here is where + // we know what charset we want to use. +#ifdef DOING_CHARSET + if (reallyDredd) + dest_csid = INTL_DefaultNewsCharSetID(dest_csid); + else + dest_csid = INTL_DefaultMailCharSetID(dest_csid); +#endif + + // do all sorts of crazy escaping + convertedValue = reallyDredd + ? EscapeSearchUrl(searchTermValue.get()) + : EscapeImapSearchProtocol(searchTermValue.get()); + useQuotes = + ((!reallyDredd || + (nsDependentString(convertedValue).FindChar(char16_t(' ')) != + -1)) && + (attrib != nsMsgSearchAttrib::Keywords)); + // now convert to char* and escape quoted_specials + nsAutoCString valueStr; + nsresult rv = nsMsgI18NConvertFromUnicode( + NS_LossyConvertUTF16toASCII(destCharset), + nsDependentString(convertedValue), valueStr); + if (NS_SUCCEEDED(rv)) { + const char* vptr = valueStr.get(); + // max escaped length is one extra character for every character in the + // cmd. + mozilla::UniquePtr<char[]> newValue = + mozilla::MakeUnique<char[]>(2 * strlen(vptr) + 1); + if (newValue) { + char* p = newValue.get(); + while (1) { + char ch = *vptr++; + if (!ch) break; + if ((useQuotes ? ch == '"' : 0) || ch == '\\') *p++ = '\\'; + *p++ = ch; + } + *p = '\0'; + value = strdup(newValue.get()); // realloc down to smaller size + } + } else + value = strdup(""); + free(convertedValue); + valueWasAllocated = true; + } + } + + // this should be rewritten to use nsCString + int subLen = (value ? strlen(value) : 0) + (useNot ? strlen(m_kImapNot) : 0) + + strlen(m_kImapHeader); + int len = + strlen(whichMnemonic) + subLen + (useQuotes ? 2 : 0) + + (orHeaderMnemonic + ? (subLen + strlen(m_kImapOr) + strlen(orHeaderMnemonic) + 2 /*""*/) + : 0) + + 10; // add slough for imap string literals + char* encoding = new char[len]; + if (encoding) { + encoding[0] = '\0'; + // Remember: if ToOrCC and useNot then the expression becomes NOT To AND Not + // CC as opposed to (NOT TO) || (NOT CC) + if (orHeaderMnemonic && !useNot) PL_strcat(encoding, m_kImapOr); + if (useNot) PL_strcat(encoding, m_kImapNot); + if (!arbitraryHeader.IsEmpty()) PL_strcat(encoding, m_kImapHeader); + PL_strcat(encoding, whichMnemonic); + if (!ignoreValue) + err = EncodeImapValue(encoding, value, useQuotes, reallyDredd); + + if (orHeaderMnemonic) { + if (useNot) PL_strcat(encoding, m_kImapNot); + + PL_strcat(encoding, m_kImapHeader); + + PL_strcat(encoding, orHeaderMnemonic); + if (!ignoreValue) + err = EncodeImapValue(encoding, value, useQuotes, reallyDredd); + } + + // kmcentee, don't let the encoding end with whitespace, + // this throws off later url STRCMP + if (*encoding && *(encoding + strlen(encoding) - 1) == ' ') + *(encoding + strlen(encoding) - 1) = '\0'; + } + + if (value && valueWasAllocated) free(value); + + *ppOutTerm = encoding; + + return err; +} + +nsresult nsMsgSearchAdapter::EncodeImapValue(char* encoding, const char* value, + bool useQuotes, bool reallyDredd) { + // By NNTP RFC, SEARCH HEADER SUBJECT "" is legal and means 'find messages + // without a subject header' + if (!reallyDredd) { + // By IMAP RFC, SEARCH HEADER SUBJECT "" is illegal and will generate an + // error from the server + if (!value || !value[0]) return NS_ERROR_NULL_POINTER; + } + + if (!NS_IsAscii(value)) { + nsAutoCString lengthStr; + PL_strcat(encoding, "{"); + lengthStr.AppendInt((int32_t)strlen(value)); + PL_strcat(encoding, lengthStr.get()); + PL_strcat(encoding, "}" CRLF); + PL_strcat(encoding, value); + return NS_OK; + } + if (useQuotes) PL_strcat(encoding, "\""); + PL_strcat(encoding, value); + if (useQuotes) PL_strcat(encoding, "\""); + + return NS_OK; +} + +nsresult nsMsgSearchAdapter::EncodeImap( + char** ppOutEncoding, nsTArray<RefPtr<nsIMsgSearchTerm>> const& searchTerms, + const char16_t* srcCharset, const char16_t* destCharset, bool reallyDredd) { + // i've left the old code (before using CBoolExpression for debugging purposes + // to make sure that the new code generates the same encoding string as the + // old code..... + + nsresult err = NS_OK; + *ppOutEncoding = nullptr; + + // create our expression + nsMsgSearchBoolExpression* expression = new nsMsgSearchBoolExpression(); + if (!expression) return NS_ERROR_OUT_OF_MEMORY; + + for (nsIMsgSearchTerm* pTerm : searchTerms) { + bool matchAll; + pTerm->GetMatchAll(&matchAll); + if (matchAll) continue; + char* termEncoding; + err = EncodeImapTerm(pTerm, reallyDredd, srcCharset, destCharset, + &termEncoding); + if (NS_SUCCEEDED(err) && nullptr != termEncoding) { + expression = nsMsgSearchBoolExpression::AddSearchTerm(expression, pTerm, + termEncoding); + delete[] termEncoding; + } else { + break; + } + } + + if (NS_SUCCEEDED(err)) { + // Catenate the intermediate encodings together into a big string + nsAutoCString encodingBuff; + + if (!reallyDredd) encodingBuff.Append(m_kImapUnDeleted); + + expression->GenerateEncodeStr(&encodingBuff); + *ppOutEncoding = ToNewCString(encodingBuff); + } + + delete expression; + + return err; +} + +char* nsMsgSearchAdapter::TransformSpacesToStars( + const char* spaceString, msg_TransformType transformType) { + char* starString; + + if (transformType == kOverwrite) { + if ((starString = strdup(spaceString)) != nullptr) { + char* star = starString; + while ((star = PL_strchr(star, ' ')) != nullptr) *star = '*'; + } + } else { + int i, count; + + for (i = 0, count = 0; spaceString[i];) { + if (spaceString[i++] == ' ') { + count++; + while (spaceString[i] && spaceString[i] == ' ') i++; + } + } + + if (transformType == kSurround) count *= 2; + + if (count > 0) { + if ((starString = (char*)PR_Malloc(i + count + 1)) != nullptr) { + int j; + + for (i = 0, j = 0; spaceString[i];) { + if (spaceString[i] == ' ') { + starString[j++] = '*'; + starString[j++] = ' '; + if (transformType == kSurround) starString[j++] = '*'; + + i++; + while (spaceString[i] && spaceString[i] == ' ') i++; + } else + starString[j++] = spaceString[i++]; + } + starString[j] = 0; + } + } else + starString = strdup(spaceString); + } + + return starString; +} + +//----------------------------------------------------------------------------- +//------------------- Validity checking for menu items etc. ------------------- +//----------------------------------------------------------------------------- + +nsMsgSearchValidityTable::nsMsgSearchValidityTable() { + // Set everything to be unavailable and disabled + for (int i = 0; i < nsMsgSearchAttrib::kNumMsgSearchAttributes; i++) + for (int j = 0; j < nsMsgSearchOp::kNumMsgSearchOperators; j++) { + SetAvailable(i, j, false); + SetEnabled(i, j, false); + SetValidButNotShown(i, j, false); + } + m_numAvailAttribs = + 0; // # of attributes marked with at least one available operator + // assume default is Subject, which it is for mail and news search + // it's not for LDAP, so we'll call SetDefaultAttrib() + m_defaultAttrib = nsMsgSearchAttrib::Subject; +} + +NS_IMPL_ISUPPORTS(nsMsgSearchValidityTable, nsIMsgSearchValidityTable) + +nsresult nsMsgSearchValidityTable::GetNumAvailAttribs(int32_t* aResult) { + m_numAvailAttribs = 0; + for (int i = 0; i < nsMsgSearchAttrib::kNumMsgSearchAttributes; i++) + for (int j = 0; j < nsMsgSearchOp::kNumMsgSearchOperators; j++) { + bool available; + GetAvailable(i, j, &available); + if (available) { + m_numAvailAttribs++; + break; + } + } + *aResult = m_numAvailAttribs; + return NS_OK; +} + +nsresult nsMsgSearchValidityTable::GetAvailableAttributes( + nsTArray<nsMsgSearchAttribValue>& aResult) { + aResult.Clear(); + int32_t i, j; + for (i = 0; i < nsMsgSearchAttrib::kNumMsgSearchAttributes; i++) { + for (j = 0; j < nsMsgSearchOp::kNumMsgSearchOperators; j++) { + if (m_table[i][j].bitAvailable) { + aResult.AppendElement(static_cast<nsMsgSearchAttribValue>(i)); + break; + } + } + } + return NS_OK; +} + +nsresult nsMsgSearchValidityTable::GetAvailableOperators( + nsMsgSearchAttribValue aAttribute, nsTArray<nsMsgSearchOpValue>& aResult) { + aResult.Clear(); + + nsMsgSearchAttribValue attr; + if (aAttribute == nsMsgSearchAttrib::Default) + attr = m_defaultAttrib; + else + attr = std::min(aAttribute, + (nsMsgSearchAttribValue)nsMsgSearchAttrib::OtherHeader); + + int32_t i; + for (i = 0; i < nsMsgSearchOp::kNumMsgSearchOperators; i++) { + if (m_table[attr][i].bitAvailable) { + aResult.AppendElement(static_cast<nsMsgSearchOpValue>(i)); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchValidityTable::SetDefaultAttrib(nsMsgSearchAttribValue aAttribute) { + m_defaultAttrib = aAttribute; + return NS_OK; +} + +nsMsgSearchValidityManager::nsMsgSearchValidityManager() {} + +nsMsgSearchValidityManager::~nsMsgSearchValidityManager() { + // tables released by nsCOMPtr +} + +NS_IMPL_ISUPPORTS(nsMsgSearchValidityManager, nsIMsgSearchValidityManager) + +//----------------------------------------------------------------------------- +// Bottleneck accesses to the objects so we can allocate and initialize them +// lazily. This way, there's no heap overhead for the validity tables until the +// user actually searches that scope. +//----------------------------------------------------------------------------- + +NS_IMETHODIMP nsMsgSearchValidityManager::GetTable( + int whichTable, nsIMsgSearchValidityTable** ppOutTable) { + NS_ENSURE_ARG_POINTER(ppOutTable); + + nsresult rv; + *ppOutTable = nullptr; + + nsCOMPtr<nsIPrefBranch> pref(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + nsCString customHeaders; + if (NS_SUCCEEDED(rv)) pref->GetCharPref(PREF_CUSTOM_HEADERS, customHeaders); + + switch (whichTable) { + case nsMsgSearchScope::offlineMail: + if (!m_offlineMailTable) rv = InitOfflineMailTable(); + if (m_offlineMailTable) + rv = SetOtherHeadersInTable(m_offlineMailTable, customHeaders.get()); + *ppOutTable = m_offlineMailTable; + break; + case nsMsgSearchScope::offlineMailFilter: + if (!m_offlineMailFilterTable) rv = InitOfflineMailFilterTable(); + if (m_offlineMailFilterTable) + rv = SetOtherHeadersInTable(m_offlineMailFilterTable, + customHeaders.get()); + *ppOutTable = m_offlineMailFilterTable; + break; + case nsMsgSearchScope::onlineMail: + if (!m_onlineMailTable) rv = InitOnlineMailTable(); + if (m_onlineMailTable) + rv = SetOtherHeadersInTable(m_onlineMailTable, customHeaders.get()); + *ppOutTable = m_onlineMailTable; + break; + case nsMsgSearchScope::onlineMailFilter: + if (!m_onlineMailFilterTable) rv = InitOnlineMailFilterTable(); + if (m_onlineMailFilterTable) + rv = SetOtherHeadersInTable(m_onlineMailFilterTable, + customHeaders.get()); + *ppOutTable = m_onlineMailFilterTable; + break; + case nsMsgSearchScope::news: + if (!m_newsTable) rv = InitNewsTable(); + if (m_newsTable) + rv = SetOtherHeadersInTable(m_newsTable, customHeaders.get()); + *ppOutTable = m_newsTable; + break; + case nsMsgSearchScope::newsFilter: + if (!m_newsFilterTable) rv = InitNewsFilterTable(); + if (m_newsFilterTable) + rv = SetOtherHeadersInTable(m_newsFilterTable, customHeaders.get()); + *ppOutTable = m_newsFilterTable; + break; + case nsMsgSearchScope::localNews: + if (!m_localNewsTable) rv = InitLocalNewsTable(); + if (m_localNewsTable) + rv = SetOtherHeadersInTable(m_localNewsTable, customHeaders.get()); + *ppOutTable = m_localNewsTable; + break; + case nsMsgSearchScope::localNewsJunk: + if (!m_localNewsJunkTable) rv = InitLocalNewsJunkTable(); + if (m_localNewsJunkTable) + rv = SetOtherHeadersInTable(m_localNewsJunkTable, customHeaders.get()); + *ppOutTable = m_localNewsJunkTable; + break; + case nsMsgSearchScope::localNewsBody: + if (!m_localNewsBodyTable) rv = InitLocalNewsBodyTable(); + if (m_localNewsBodyTable) + rv = SetOtherHeadersInTable(m_localNewsBodyTable, customHeaders.get()); + *ppOutTable = m_localNewsBodyTable; + break; + case nsMsgSearchScope::localNewsJunkBody: + if (!m_localNewsJunkBodyTable) rv = InitLocalNewsJunkBodyTable(); + if (m_localNewsJunkBodyTable) + rv = SetOtherHeadersInTable(m_localNewsJunkBodyTable, + customHeaders.get()); + *ppOutTable = m_localNewsJunkBodyTable; + break; + + case nsMsgSearchScope::onlineManual: + if (!m_onlineManualFilterTable) rv = InitOnlineManualFilterTable(); + if (m_onlineManualFilterTable) + rv = SetOtherHeadersInTable(m_onlineManualFilterTable, + customHeaders.get()); + *ppOutTable = m_onlineManualFilterTable; + break; + case nsMsgSearchScope::LDAP: + if (!m_ldapTable) rv = InitLdapTable(); + *ppOutTable = m_ldapTable; + break; + case nsMsgSearchScope::LDAPAnd: + if (!m_ldapAndTable) rv = InitLdapAndTable(); + *ppOutTable = m_ldapAndTable; + break; + case nsMsgSearchScope::LocalAB: + if (!m_localABTable) rv = InitLocalABTable(); + *ppOutTable = m_localABTable; + break; + case nsMsgSearchScope::LocalABAnd: + if (!m_localABAndTable) rv = InitLocalABAndTable(); + *ppOutTable = m_localABAndTable; + break; + default: + NS_ASSERTION(false, "invalid table type"); + rv = NS_MSG_ERROR_INVALID_SEARCH_TERM; + } + + NS_IF_ADDREF(*ppOutTable); // Was populated from member variable. + return rv; +} + +// mapping between ordered attribute values, and property strings +// see search-attributes.properties +static struct { + nsMsgSearchAttribValue id; + const char* property; +} nsMsgSearchAttribMap[] = { + {nsMsgSearchAttrib::Subject, "Subject"}, + {nsMsgSearchAttrib::Sender, "From"}, + {nsMsgSearchAttrib::Body, "Body"}, + {nsMsgSearchAttrib::Date, "Date"}, + {nsMsgSearchAttrib::Priority, "Priority"}, + {nsMsgSearchAttrib::MsgStatus, "Status"}, + {nsMsgSearchAttrib::To, "To"}, + {nsMsgSearchAttrib::CC, "Cc"}, + {nsMsgSearchAttrib::ToOrCC, "ToOrCc"}, + {nsMsgSearchAttrib::AgeInDays, "AgeInDays"}, + {nsMsgSearchAttrib::Size, "SizeKB"}, + {nsMsgSearchAttrib::Keywords, "Tags"}, + {nsMsgSearchAttrib::Name, "AnyName"}, + {nsMsgSearchAttrib::DisplayName, "DisplayName"}, + {nsMsgSearchAttrib::Nickname, "Nickname"}, + {nsMsgSearchAttrib::ScreenName, "ScreenName"}, + {nsMsgSearchAttrib::Email, "Email"}, + {nsMsgSearchAttrib::AdditionalEmail, "AdditionalEmail"}, + {nsMsgSearchAttrib::PhoneNumber, "AnyNumber"}, + {nsMsgSearchAttrib::WorkPhone, "WorkPhone"}, + {nsMsgSearchAttrib::HomePhone, "HomePhone"}, + {nsMsgSearchAttrib::Fax, "Fax"}, + {nsMsgSearchAttrib::Pager, "Pager"}, + {nsMsgSearchAttrib::Mobile, "Mobile"}, + {nsMsgSearchAttrib::City, "City"}, + {nsMsgSearchAttrib::Street, "Street"}, + {nsMsgSearchAttrib::Title, "Title"}, + {nsMsgSearchAttrib::Organization, "Organization"}, + {nsMsgSearchAttrib::Department, "Department"}, + {nsMsgSearchAttrib::AllAddresses, "FromToCcOrBcc"}, + {nsMsgSearchAttrib::JunkScoreOrigin, "JunkScoreOrigin"}, + {nsMsgSearchAttrib::JunkPercent, "JunkPercent"}, + {nsMsgSearchAttrib::HasAttachmentStatus, "AttachmentStatus"}, + {nsMsgSearchAttrib::JunkStatus, "JunkStatus"}, + {nsMsgSearchAttrib::OtherHeader, "Customize"}, + // the last id is -1 to denote end of table + {-1, ""}}; + +NS_IMETHODIMP +nsMsgSearchValidityManager::GetAttributeProperty( + nsMsgSearchAttribValue aSearchAttribute, nsAString& aProperty) { + for (int32_t i = 0; nsMsgSearchAttribMap[i].id >= 0; ++i) { + if (nsMsgSearchAttribMap[i].id == aSearchAttribute) { + aProperty.Assign(NS_ConvertUTF8toUTF16(nsMsgSearchAttribMap[i].property)); + return NS_OK; + } + } + return NS_ERROR_FAILURE; +} + +nsresult nsMsgSearchValidityManager::NewTable( + nsIMsgSearchValidityTable** aTable) { + NS_ENSURE_ARG_POINTER(aTable); + NS_ADDREF(*aTable = new nsMsgSearchValidityTable); + return NS_OK; +} + +nsresult nsMsgSearchValidityManager::SetOtherHeadersInTable( + nsIMsgSearchValidityTable* aTable, const char* customHeaders) { + uint32_t customHeadersLength = strlen(customHeaders); + uint32_t numHeaders = 0; + if (customHeadersLength) { + nsAutoCString hdrStr(customHeaders); + hdrStr.StripWhitespace(); // remove whitespace before parsing + char* newStr = hdrStr.BeginWriting(); + char* token = NS_strtok(":", &newStr); + while (token) { + numHeaders++; + token = NS_strtok(":", &newStr); + } + } + + NS_ASSERTION(nsMsgSearchAttrib::OtherHeader + numHeaders < + nsMsgSearchAttrib::kNumMsgSearchAttributes, + "more headers than the table can hold"); + + uint32_t maxHdrs = + std::min(nsMsgSearchAttrib::OtherHeader + numHeaders + 1, + (uint32_t)nsMsgSearchAttrib::kNumMsgSearchAttributes); + for (uint32_t i = nsMsgSearchAttrib::OtherHeader + 1; i < maxHdrs; i++) { + // clang-format off + aTable->SetAvailable(i, nsMsgSearchOp::Contains, 1); // added for arbitrary headers + aTable->SetEnabled (i, nsMsgSearchOp::Contains, 1); + aTable->SetAvailable(i, nsMsgSearchOp::DoesntContain, 1); + aTable->SetEnabled (i, nsMsgSearchOp::DoesntContain, 1); + aTable->SetAvailable(i, nsMsgSearchOp::Is, 1); + aTable->SetEnabled (i, nsMsgSearchOp::Is, 1); + aTable->SetAvailable(i, nsMsgSearchOp::Isnt, 1); + aTable->SetEnabled (i, nsMsgSearchOp::Isnt, 1); + // clang-format on + } + // because custom headers can change; so reset the table for those which are + // no longer used. + for (uint32_t j = maxHdrs; j < nsMsgSearchAttrib::kNumMsgSearchAttributes; + j++) { + for (uint32_t k = 0; k < nsMsgSearchOp::kNumMsgSearchOperators; k++) { + aTable->SetAvailable(j, k, 0); + aTable->SetEnabled(j, k, 0); + } + } + return NS_OK; +} + +nsresult nsMsgSearchValidityManager::EnableDirectoryAttribute( + nsIMsgSearchValidityTable* table, nsMsgSearchAttribValue aSearchAttrib) { + // clang-format off + table->SetAvailable(aSearchAttrib, nsMsgSearchOp::Contains, 1); + table->SetEnabled (aSearchAttrib, nsMsgSearchOp::Contains, 1); + table->SetAvailable(aSearchAttrib, nsMsgSearchOp::DoesntContain, 1); + table->SetEnabled (aSearchAttrib, nsMsgSearchOp::DoesntContain, 1); + table->SetAvailable(aSearchAttrib, nsMsgSearchOp::Is, 1); + table->SetEnabled (aSearchAttrib, nsMsgSearchOp::Is, 1); + table->SetAvailable(aSearchAttrib, nsMsgSearchOp::Isnt, 1); + table->SetEnabled (aSearchAttrib, nsMsgSearchOp::Isnt, 1); + table->SetAvailable(aSearchAttrib, nsMsgSearchOp::BeginsWith, 1); + table->SetEnabled (aSearchAttrib, nsMsgSearchOp::BeginsWith, 1); + table->SetAvailable(aSearchAttrib, nsMsgSearchOp::EndsWith, 1); + table->SetEnabled (aSearchAttrib, nsMsgSearchOp::EndsWith, 1); + table->SetAvailable(aSearchAttrib, nsMsgSearchOp::SoundsLike, 1); + table->SetEnabled (aSearchAttrib, nsMsgSearchOp::SoundsLike, 1); + // clang-format on + return NS_OK; +} + +nsresult nsMsgSearchValidityManager::InitLdapTable() { + NS_ASSERTION(!m_ldapTable, "don't call this twice!"); + + nsresult rv = NewTable(getter_AddRefs(m_ldapTable)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetUpABTable(m_ldapTable, true); + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +nsresult nsMsgSearchValidityManager::InitLdapAndTable() { + NS_ASSERTION(!m_ldapAndTable, "don't call this twice!"); + + nsresult rv = NewTable(getter_AddRefs(m_ldapAndTable)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetUpABTable(m_ldapAndTable, false); + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +nsresult nsMsgSearchValidityManager::InitLocalABTable() { + NS_ASSERTION(!m_localABTable, "don't call this twice!"); + + nsresult rv = NewTable(getter_AddRefs(m_localABTable)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetUpABTable(m_localABTable, true); + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +nsresult nsMsgSearchValidityManager::InitLocalABAndTable() { + NS_ASSERTION(!m_localABAndTable, "don't call this twice!"); + + nsresult rv = NewTable(getter_AddRefs(m_localABAndTable)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetUpABTable(m_localABAndTable, false); + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +nsresult nsMsgSearchValidityManager::SetUpABTable( + nsIMsgSearchValidityTable* aTable, bool isOrTable) { + nsresult rv = aTable->SetDefaultAttrib( + isOrTable ? nsMsgSearchAttrib::Name : nsMsgSearchAttrib::DisplayName); + NS_ENSURE_SUCCESS(rv, rv); + + if (isOrTable) { + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Name); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::PhoneNumber); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::DisplayName); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Email); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::AdditionalEmail); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::ScreenName); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Street); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::City); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Title); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Organization); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Department); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Nickname); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::WorkPhone); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::HomePhone); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Fax); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Pager); + NS_ENSURE_SUCCESS(rv, rv); + + rv = EnableDirectoryAttribute(aTable, nsMsgSearchAttrib::Mobile); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} diff --git a/comm/mailnews/search/src/nsMsgSearchImap.h b/comm/mailnews/search/src/nsMsgSearchImap.h new file mode 100644 index 0000000000..92dbed8dcc --- /dev/null +++ b/comm/mailnews/search/src/nsMsgSearchImap.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgSearchImap_h__ +# include "mozilla/Attributes.h" +# include "nsMsgSearchAdapter.h" + +//----------------------------------------------------------------------------- +//---------- Adapter class for searching online (IMAP) folders ---------------- +//----------------------------------------------------------------------------- + +class nsMsgSearchOnlineMail : public nsMsgSearchAdapter { + public: + nsMsgSearchOnlineMail(nsMsgSearchScopeTerm* scope, + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList); + virtual ~nsMsgSearchOnlineMail(); + + NS_IMETHOD ValidateTerms() override; + NS_IMETHOD Search(bool* aDone) override; + NS_IMETHOD GetEncoding(char** result) override; + NS_IMETHOD AddResultElement(nsIMsgDBHdr*) override; + + static nsresult Encode(nsCString& ppEncoding, + nsTArray<RefPtr<nsIMsgSearchTerm>> const& searchTerms, + const char16_t* destCharset, + nsIMsgSearchScopeTerm* scope); + + protected: + nsCString m_encoding; +}; + +#endif diff --git a/comm/mailnews/search/src/nsMsgSearchNews.cpp b/comm/mailnews/search/src/nsMsgSearchNews.cpp new file mode 100644 index 0000000000..022a80e79f --- /dev/null +++ b/comm/mailnews/search/src/nsMsgSearchNews.cpp @@ -0,0 +1,452 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#include "msgCore.h" +#include "nsMsgSearchAdapter.h" +#include "nsUnicharUtils.h" +#include "nsMsgSearchScopeTerm.h" +#include "nsMsgResultElement.h" +#include "nsMsgSearchTerm.h" +#include "nsIMsgHdr.h" +#include "nsMsgSearchNews.h" +#include "nsIDBFolderInfo.h" +#include "prprf.h" +#include "nsIMsgDatabase.h" +#include "nsMemory.h" +#include <ctype.h> + +// Implementation of search for IMAP mail folders + +// Implementation of search for newsgroups + +//----------------------------------------------------------------------------- +//----------- Adapter class for searching XPAT-capable news servers ----------- +//----------------------------------------------------------------------------- + +const char* nsMsgSearchNews::m_kNntpFrom = "FROM "; +const char* nsMsgSearchNews::m_kNntpSubject = "SUBJECT "; +const char* nsMsgSearchNews::m_kTermSeparator = "/"; + +nsMsgSearchNews::nsMsgSearchNews( + nsMsgSearchScopeTerm* scope, + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList) + : nsMsgSearchAdapter(scope, termList) { + m_searchType = ST_UNINITIALIZED; +} + +nsMsgSearchNews::~nsMsgSearchNews() {} + +nsresult nsMsgSearchNews::ValidateTerms() { + nsresult err = nsMsgSearchAdapter::ValidateTerms(); + if (NS_OK == err) { + err = Encode(&m_encoding); + } + + return err; +} + +nsresult nsMsgSearchNews::Search(bool* aDone) { + // the state machine runs in the news: handler + nsresult err = NS_ERROR_NOT_IMPLEMENTED; + return err; +} + +char16_t* nsMsgSearchNews::EncodeToWildmat(const char16_t* value) { + // Here we take advantage of XPAT's use of the wildmat format, which allows + // a case-insensitive match by specifying each case possibility for each + // character So, "FooBar" is encoded as "[Ff][Oo][Bb][Aa][Rr]" + + char16_t* caseInsensitiveValue = + (char16_t*)moz_xmalloc(sizeof(char16_t) * ((4 * NS_strlen(value)) + 1)); + if (caseInsensitiveValue) { + char16_t* walkValue = caseInsensitiveValue; + while (*value) { + if (isalpha(*value)) { + *walkValue++ = (char16_t)'['; + *walkValue++ = ToUpperCase((char16_t)*value); + *walkValue++ = ToLowerCase((char16_t)*value); + *walkValue++ = (char16_t)']'; + } else + *walkValue++ = *value; + value++; + } + *walkValue = 0; + } + return caseInsensitiveValue; +} + +char* nsMsgSearchNews::EncodeTerm(nsIMsgSearchTerm* term) { + // Develop an XPAT-style encoding for the search term + + NS_ASSERTION(term, "null term"); + if (!term) return nullptr; + + // Find a string to represent the attribute + const char* attribEncoding = nullptr; + nsMsgSearchAttribValue attrib; + + term->GetAttrib(&attrib); + + switch (attrib) { + case nsMsgSearchAttrib::Sender: + attribEncoding = m_kNntpFrom; + break; + case nsMsgSearchAttrib::Subject: + attribEncoding = m_kNntpSubject; + break; + default: + nsCString header; + term->GetArbitraryHeader(header); + if (header.IsEmpty()) { + NS_ASSERTION(false, "malformed search"); // malformed search term? + return nullptr; + } + attribEncoding = header.get(); + } + + // Build a string to represent the string pattern + bool leadingStar = false; + bool trailingStar = false; + nsMsgSearchOpValue op; + term->GetOp(&op); + + switch (op) { + case nsMsgSearchOp::Contains: + leadingStar = true; + trailingStar = true; + break; + case nsMsgSearchOp::Is: + break; + case nsMsgSearchOp::BeginsWith: + trailingStar = true; + break; + case nsMsgSearchOp::EndsWith: + leadingStar = true; + break; + default: + NS_ASSERTION(false, "malformed search"); // malformed search term? + return nullptr; + } + + // ### i18N problem Get the csid from FE, which is the correct csid for term + // int16 wincsid = INTL_GetCharSetID(INTL_DefaultTextWidgetCsidSel); + + // Do INTL_FormatNNTPXPATInRFC1522Format trick for non-ASCII string + // unsigned char *intlNonRFC1522Value = INTL_FormatNNTPXPATInNonRFC1522Format + // (wincsid, (unsigned char*)term->m_value.u.string); + nsCOMPtr<nsIMsgSearchValue> searchValue; + + nsresult rv = term->GetValue(getter_AddRefs(searchValue)); + if (NS_FAILED(rv) || !searchValue) return nullptr; + + nsString intlNonRFC1522Value; + rv = searchValue->GetStr(intlNonRFC1522Value); + if (NS_FAILED(rv) || intlNonRFC1522Value.IsEmpty()) return nullptr; + + char16_t* caseInsensitiveValue = EncodeToWildmat(intlNonRFC1522Value.get()); + if (!caseInsensitiveValue) return nullptr; + + // TO DO: Do INTL_FormatNNTPXPATInRFC1522Format trick for non-ASCII string + // Unfortunately, we currently do not handle xxx or xxx search in XPAT + // Need to add the INTL_FormatNNTPXPATInRFC1522Format call after we can do + // that so we should search a string in either RFC1522 format and non-RFC1522 + // format + + char16_t* escapedValue = EscapeSearchUrl(caseInsensitiveValue); + free(caseInsensitiveValue); + if (!escapedValue) return nullptr; + + nsAutoCString pattern; + + if (leadingStar) pattern.Append('*'); + pattern.Append(NS_ConvertUTF16toUTF8(escapedValue)); + if (trailingStar) pattern.Append('*'); + + // Combine the XPAT command syntax with the attribute and the pattern to + // form the term encoding + const char xpatTemplate[] = "XPAT %s 1- %s"; + int termLength = (sizeof(xpatTemplate) - 1) + strlen(attribEncoding) + + pattern.Length() + 1; + char* termEncoding = new char[termLength]; + if (termEncoding) + PR_snprintf(termEncoding, termLength, xpatTemplate, attribEncoding, + pattern.get()); + + return termEncoding; +} + +nsresult nsMsgSearchNews::GetEncoding(char** result) { + NS_ENSURE_ARG(result); + *result = ToNewCString(m_encoding); + return (*result) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +nsresult nsMsgSearchNews::Encode(nsCString* outEncoding) { + NS_ASSERTION(outEncoding, "no out encoding"); + if (!outEncoding) return NS_ERROR_NULL_POINTER; + + nsresult err = NS_OK; + + uint32_t numTerms = m_searchTerms.Length(); + + char** intermediateEncodings = new char*[numTerms]; + if (intermediateEncodings) { + // Build an XPAT command for each term + int encodingLength = 0; + for (uint32_t i = 0; i < numTerms; i++) { + nsIMsgSearchTerm* pTerm = m_searchTerms[i]; + // set boolean OR term if any of the search terms are an OR...this only + // works if we are using homogeneous boolean operators. + bool isBooleanOpAnd; + pTerm->GetBooleanAnd(&isBooleanOpAnd); + m_searchType = isBooleanOpAnd ? ST_AND_SEARCH : ST_OR_SEARCH; + + intermediateEncodings[i] = EncodeTerm(pTerm); + if (intermediateEncodings[i]) + encodingLength += + strlen(intermediateEncodings[i]) + strlen(m_kTermSeparator); + } + encodingLength += strlen("?search"); + // Combine all the term encodings into one big encoding + char* encoding = new char[encodingLength + 1]; + if (encoding) { + PL_strcpy(encoding, "?search"); + + for (uint32_t i = 0; i < numTerms; i++) { + if (intermediateEncodings[i]) { + PL_strcat(encoding, m_kTermSeparator); + PL_strcat(encoding, intermediateEncodings[i]); + delete[] intermediateEncodings[i]; + } + } + *outEncoding = encoding; + } else + err = NS_ERROR_OUT_OF_MEMORY; + } else + err = NS_ERROR_OUT_OF_MEMORY; + delete[] intermediateEncodings; + + return err; +} + +NS_IMETHODIMP nsMsgSearchNews::AddHit(nsMsgKey key) { + m_candidateHits.AppendElement(key); + return NS_OK; +} + +/* void CurrentUrlDone (in nsresult exitCode); */ +NS_IMETHODIMP nsMsgSearchNews::CurrentUrlDone(nsresult exitCode) { + CollateHits(); + ReportHits(); + return NS_OK; +} + +#if 0 // need to switch this to a notify stop loading handler, I think. +void nsMsgSearchNews::PreExitFunction (URL_Struct * /*url*/, int status, MWContext *context) +{ + MSG_SearchFrame *frame = MSG_SearchFrame::FromContext (context); + nsMsgSearchNews *adapter = (nsMsgSearchNews*) frame->GetRunningAdapter(); + adapter->CollateHits(); + adapter->ReportHits(); + + if (status == MK_INTERRUPTED) + { + adapter->Abort(); + frame->EndCylonMode(); + } + else + { + frame->m_idxRunningScope++; + if (frame->m_idxRunningScope >= frame->m_scopeList.Count()) + frame->EndCylonMode(); + } +} +#endif // 0 + +void nsMsgSearchNews::CollateHits() { + // Since the XPAT commands are processed one at a time, the result set for the + // entire query is the intersection of results for each XPAT command if an AND + // search, otherwise we want the union of all the search hits (minus the + // duplicates of course). + + uint32_t size = m_candidateHits.Length(); + if (!size) return; + + // Sort the article numbers first, so it's easy to tell how many hits + // on a given article we got + m_candidateHits.Sort(); + + // For an OR search we only need to count the first occurrence of a candidate. + uint32_t termCount = 1; + MOZ_ASSERT(m_searchType != ST_UNINITIALIZED, + "m_searchType accessed without being set"); + if (m_searchType == ST_AND_SEARCH) { + // We have a traditional AND search which must be collated. In order to + // get promoted into the hits list, a candidate article number must appear + // in the results of each XPAT command. So if we fire 3 XPAT commands (one + // per search term), the article number must appear 3 times. If it appears + // fewer than 3 times, it matched some search terms, but not all. + termCount = m_searchTerms.Length(); + } + uint32_t candidateCount = 0; + uint32_t candidate = m_candidateHits[0]; + for (uint32_t index = 0; index < size; ++index) { + uint32_t possibleCandidate = m_candidateHits[index]; + if (candidate == possibleCandidate) { + ++candidateCount; + } else { + candidateCount = 1; + candidate = possibleCandidate; + } + if (candidateCount == termCount) m_hits.AppendElement(candidate); + } +} + +void nsMsgSearchNews::ReportHits() { + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsCOMPtr<nsIMsgFolder> scopeFolder; + + nsresult err = m_scope->GetFolder(getter_AddRefs(scopeFolder)); + if (NS_SUCCEEDED(err) && scopeFolder) { + err = scopeFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(db)); + } + + if (db) { + uint32_t size = m_hits.Length(); + for (uint32_t i = 0; i < size; ++i) { + nsCOMPtr<nsIMsgDBHdr> header; + + db->GetMsgHdrForKey(m_hits.ElementAt(i), getter_AddRefs(header)); + if (header) ReportHit(header, scopeFolder); + } + } +} + +// ### this should take an nsIMsgFolder instead of a string location. +void nsMsgSearchNews::ReportHit(nsIMsgDBHdr* pHeaders, nsIMsgFolder* folder) { + // this is totally filched from msg_SearchOfflineMail until I decide whether + // the right thing is to get them from the db or from NNTP + nsCOMPtr<nsIMsgSearchSession> session; + nsCOMPtr<nsIMsgFolder> scopeFolder; + m_scope->GetFolder(getter_AddRefs(scopeFolder)); + m_scope->GetSearchSession(getter_AddRefs(session)); + if (session) session->AddSearchHit(pHeaders, scopeFolder); +} + +nsresult nsMsgSearchValidityManager::InitNewsTable() { + NS_ASSERTION(nullptr == m_newsTable, "don't call this twice!"); + nsresult rv = NewTable(getter_AddRefs(m_newsTable)); + + if (NS_SUCCEEDED(rv)) { + // clang-format off + m_newsTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_newsTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + m_newsTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + m_newsTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + + m_newsTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_newsTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + m_newsTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + m_newsTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + +#if 0 + // Size should be handled after the fact... + m_newsTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_newsTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); +#endif + m_newsTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_newsTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_newsTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_newsTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + m_newsTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + // clang-format on + } + + return rv; +} + +nsresult nsMsgSearchValidityManager::InitNewsFilterTable() { + NS_ASSERTION(nullptr == m_newsFilterTable, + "news filter table already initted"); + nsresult rv = NewTable(getter_AddRefs(m_newsFilterTable)); + + if (NS_SUCCEEDED(rv)) { + // clang-format off + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Contains, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::DoesntContain, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Is, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::Isnt, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::BeginsWith, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::EndsWith, 1); + + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsInAB, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Sender, nsMsgSearchOp::IsntInAB, 1); + + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Contains, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::DoesntContain, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Is, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::Isnt, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::BeginsWith, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Subject, nsMsgSearchOp::EndsWith, 1); + + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsBefore, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::IsAfter, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Is, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Date, nsMsgSearchOp::Isnt, 1); + + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsGreaterThan, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::Size, nsMsgSearchOp::IsLessThan, 1); + + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Contains, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::DoesntContain, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Is, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::Isnt, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::BeginsWith, 1); + m_newsFilterTable->SetAvailable(nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + m_newsFilterTable->SetEnabled (nsMsgSearchAttrib::OtherHeader, nsMsgSearchOp::EndsWith, 1); + // clang-format on + } + + return rv; +} diff --git a/comm/mailnews/search/src/nsMsgSearchNews.h b/comm/mailnews/search/src/nsMsgSearchNews.h new file mode 100644 index 0000000000..38045d978a --- /dev/null +++ b/comm/mailnews/search/src/nsMsgSearchNews.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgSearchNews_h__ +# include "nsMsgSearchAdapter.h" +# include "MailNewsTypes.h" +# include "nsTArray.h" + +typedef enum search_type { + ST_UNINITIALIZED, + ST_OR_SEARCH, + ST_AND_SEARCH +} search_type; + +//----------------------------------------------------------------------------- +//---------- Adapter class for searching online (news) folders ---------------- +//----------------------------------------------------------------------------- + +class nsMsgSearchNews : public nsMsgSearchAdapter { + public: + nsMsgSearchNews(nsMsgSearchScopeTerm* scope, + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList); + virtual ~nsMsgSearchNews(); + + NS_IMETHOD ValidateTerms() override; + NS_IMETHOD Search(bool* aDone) override; + NS_IMETHOD GetEncoding(char** result) override; + NS_IMETHOD AddHit(nsMsgKey key) override; + NS_IMETHOD CurrentUrlDone(nsresult exitCode) override; + + virtual nsresult Encode(nsCString* outEncoding); + virtual char* EncodeTerm(nsIMsgSearchTerm*); + char16_t* EncodeToWildmat(const char16_t*); + + void ReportHits(); + void CollateHits(); + void ReportHit(nsIMsgDBHdr* pHeaders, nsIMsgFolder* folder); + + protected: + nsCString m_encoding; + search_type m_searchType; + + nsTArray<nsMsgKey> m_candidateHits; + nsTArray<nsMsgKey> m_hits; + + static const char* m_kNntpFrom; + static const char* m_kNntpSubject; + static const char* m_kTermSeparator; + static const char* m_kUrlPrefix; +}; + +#endif diff --git a/comm/mailnews/search/src/nsMsgSearchSession.cpp b/comm/mailnews/search/src/nsMsgSearchSession.cpp new file mode 100644 index 0000000000..51d5d363d2 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgSearchSession.cpp @@ -0,0 +1,576 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgSearchCore.h" +#include "nsMsgSearchAdapter.h" +#include "nsMsgSearchBoolExpression.h" +#include "nsMsgSearchSession.h" +#include "nsMsgResultElement.h" +#include "nsMsgSearchTerm.h" +#include "nsMsgSearchScopeTerm.h" +#include "nsIMsgMessageService.h" +#include "nsMsgUtils.h" +#include "nsIMsgSearchNotify.h" +#include "nsIMsgMailSession.h" +#include "nsIMsgWindow.h" +#include "nsMsgFolderFlags.h" +#include "nsMsgLocalSearch.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" + +NS_IMPL_ISUPPORTS(nsMsgSearchSession, nsIMsgSearchSession, nsIUrlListener, + nsISupportsWeakReference) + +nsMsgSearchSession::nsMsgSearchSession() { + m_sortAttribute = nsMsgSearchAttrib::Sender; + m_idxRunningScope = 0; + m_handlingError = false; + m_expressionTree = nullptr; + m_searchPaused = false; + m_iListener = -1; +} + +nsMsgSearchSession::~nsMsgSearchSession() { + InterruptSearch(); + delete m_expressionTree; + DestroyScopeList(); + DestroyTermList(); +} + +NS_IMETHODIMP +nsMsgSearchSession::AddSearchTerm(nsMsgSearchAttribValue attrib, + nsMsgSearchOpValue op, + nsIMsgSearchValue* value, bool BooleanANDp, + const char* customString) { + // stupid gcc + nsMsgSearchBooleanOperator boolOp; + if (BooleanANDp) + boolOp = (nsMsgSearchBooleanOperator)nsMsgSearchBooleanOp::BooleanAND; + else + boolOp = (nsMsgSearchBooleanOperator)nsMsgSearchBooleanOp::BooleanOR; + nsMsgSearchTerm* pTerm = + new nsMsgSearchTerm(attrib, op, value, boolOp, customString); + NS_ENSURE_TRUE(pTerm, NS_ERROR_OUT_OF_MEMORY); + + m_termList.AppendElement(pTerm); + // force the expression tree to rebuild whenever we change the terms + delete m_expressionTree; + m_expressionTree = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchSession::AppendTerm(nsIMsgSearchTerm* aTerm) { + NS_ENSURE_ARG_POINTER(aTerm); + delete m_expressionTree; + m_expressionTree = nullptr; + m_termList.AppendElement(aTerm); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchSession::GetSearchTerms(nsTArray<RefPtr<nsIMsgSearchTerm>>& terms) { + terms = m_termList.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchSession::SetSearchTerms( + nsTArray<RefPtr<nsIMsgSearchTerm>> const& terms) { + m_termList = terms.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchSession::CreateTerm(nsIMsgSearchTerm** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_ADDREF(*aResult = new nsMsgSearchTerm); + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchSession::RegisterListener( + nsIMsgSearchNotify* aListener, int32_t aNotifyFlags) { + NS_ENSURE_ARG_POINTER(aListener); + m_listenerList.AppendElement(aListener); + m_listenerFlagList.AppendElement(aNotifyFlags); + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchSession::UnregisterListener( + nsIMsgSearchNotify* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + size_t listenerIndex = m_listenerList.IndexOf(aListener); + if (listenerIndex != m_listenerList.NoIndex) { + m_listenerList.RemoveElementAt(listenerIndex); + m_listenerFlagList.RemoveElementAt(listenerIndex); + + // Adjust our iterator if it is active. + // Removal of something at a higher index than the iterator does not affect + // it; we only care if the the index we were pointing at gets shifted down, + // in which case we also want to shift down. + if (m_iListener != -1 && (signed)listenerIndex <= m_iListener) + m_iListener--; + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchSession::GetNumSearchTerms(uint32_t* aNumSearchTerms) { + NS_ENSURE_ARG(aNumSearchTerms); + *aNumSearchTerms = m_termList.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchSession::GetNthSearchTerm(int32_t whichTerm, + nsMsgSearchAttribValue attrib, + nsMsgSearchOpValue op, + nsIMsgSearchValue* value) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgSearchSession::CountSearchScopes(int32_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = m_scopeList.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchSession::GetNthSearchScope(int32_t which, + nsMsgSearchScopeValue* scopeId, + nsIMsgFolder** folder) { + NS_ENSURE_ARG_POINTER(scopeId); + NS_ENSURE_ARG_POINTER(folder); + + nsMsgSearchScopeTerm* scopeTerm = m_scopeList.SafeElementAt(which, nullptr); + NS_ENSURE_ARG(scopeTerm); + + *scopeId = scopeTerm->m_attribute; + NS_IF_ADDREF(*folder = scopeTerm->m_folder); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchSession::AddScopeTerm(nsMsgSearchScopeValue scope, + nsIMsgFolder* folder) { + if (scope != nsMsgSearchScope::allSearchableGroups) { + NS_ASSERTION(folder, "need folder if not searching all groups"); + NS_ENSURE_TRUE(folder, NS_ERROR_NULL_POINTER); + } + + nsMsgSearchScopeTerm* pScopeTerm = + new nsMsgSearchScopeTerm(this, scope, folder); + NS_ENSURE_TRUE(pScopeTerm, NS_ERROR_OUT_OF_MEMORY); + + m_scopeList.AppendElement(pScopeTerm); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchSession::AddDirectoryScopeTerm(nsMsgSearchScopeValue scope) { + nsMsgSearchScopeTerm* pScopeTerm = + new nsMsgSearchScopeTerm(this, scope, nullptr); + NS_ENSURE_TRUE(pScopeTerm, NS_ERROR_OUT_OF_MEMORY); + + m_scopeList.AppendElement(pScopeTerm); + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchSession::ClearScopes() { + DestroyScopeList(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchSession::ScopeUsesCustomHeaders(nsMsgSearchScopeValue scope, + void* selection, bool forFilters, + bool* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgSearchSession::IsStringAttribute(nsMsgSearchAttribValue attrib, + bool* _retval) { + // Is this check needed? + NS_ENSURE_ARG(_retval); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgSearchSession::AddAllScopes(nsMsgSearchScopeValue attrib) { + // don't think this is needed. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgSearchSession::Search(nsIMsgWindow* aWindow) { + nsresult rv = Initialize(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgSearchNotify> listener; + m_iListener = 0; + while (m_iListener != -1 && m_iListener < (signed)m_listenerList.Length()) { + listener = m_listenerList[m_iListener]; + int32_t listenerFlags = m_listenerFlagList[m_iListener++]; + if (!listenerFlags || (listenerFlags & nsIMsgSearchSession::onNewSearch)) + listener->OnNewSearch(); + } + m_iListener = -1; + + m_msgWindowWeak = do_GetWeakReference(aWindow); + + return BeginSearching(); +} + +NS_IMETHODIMP nsMsgSearchSession::InterruptSearch() { + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak)); + if (msgWindow) { + EnableFolderNotifications(true); + if (m_idxRunningScope < m_scopeList.Length()) msgWindow->StopUrls(); + + while (m_idxRunningScope < m_scopeList.Length()) { + ReleaseFolderDBRef(); + m_idxRunningScope++; + } + // m_idxRunningScope = m_scopeList.Length() so it will make us not run + // another url + } + if (m_backgroundTimer) { + m_backgroundTimer->Cancel(); + NotifyListenersDone(NS_MSG_SEARCH_INTERRUPTED); + + m_backgroundTimer = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchSession::PauseSearch() { + if (m_backgroundTimer) { + m_backgroundTimer->Cancel(); + m_searchPaused = true; + return NS_OK; + } else + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsMsgSearchSession::ResumeSearch() { + if (m_searchPaused) { + m_searchPaused = false; + return StartTimer(); + } else + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsMsgSearchSession::GetNumResults(int32_t* aNumResults) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgSearchSession::SetWindow(nsIMsgWindow* aWindow) { + m_msgWindowWeak = do_GetWeakReference(aWindow); + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchSession::GetWindow(nsIMsgWindow** aWindow) { + NS_ENSURE_ARG_POINTER(aWindow); + *aWindow = nullptr; + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak)); + msgWindow.forget(aWindow); + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchSession::OnStartRunningUrl(nsIURI* url) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchSession::OnStopRunningUrl(nsIURI* url, + nsresult aExitCode) { + nsCOMPtr<nsIMsgSearchAdapter> runningAdapter; + + nsresult rv = GetRunningAdapter(getter_AddRefs(runningAdapter)); + // tell the current adapter that the current url has run. + if (NS_SUCCEEDED(rv) && runningAdapter) { + runningAdapter->CurrentUrlDone(aExitCode); + EnableFolderNotifications(true); + ReleaseFolderDBRef(); + } + if (++m_idxRunningScope < m_scopeList.Length()) + DoNextSearch(); + else + NotifyListenersDone(aExitCode); + return NS_OK; +} + +nsresult nsMsgSearchSession::Initialize() { + // Loop over scope terms, initializing an adapter per term. This + // architecture is necessitated by two things: + // 1. There might be more than one kind of adapter per if online + // *and* offline mail mail folders are selected, or if newsgroups + // belonging to Dredd *and* INN are selected + // 2. Most of the protocols are only capable of searching one scope at a + // time, so we'll do each scope in a separate adapter on the client + + nsMsgSearchScopeTerm* scopeTerm = nullptr; + nsresult rv = NS_OK; + + uint32_t numTerms = m_termList.Length(); + // Ensure that the FE has added scopes and terms to this search + NS_ASSERTION(numTerms > 0, "no terms to search!"); + if (numTerms == 0) return NS_MSG_ERROR_NO_SEARCH_VALUES; + + // if we don't have any search scopes to search, return that code. + if (m_scopeList.Length() == 0) return NS_MSG_ERROR_INVALID_SEARCH_SCOPE; + + m_runningUrl.Truncate(); // clear out old url, if any. + m_idxRunningScope = 0; + + // If this term list (loosely specified here by the first term) should be + // scheduled in parallel, build up a list of scopes to do the round-robin + // scheduling + for (uint32_t i = 0; i < m_scopeList.Length() && NS_SUCCEEDED(rv); i++) { + scopeTerm = m_scopeList.ElementAt(i); + // NS_ASSERTION(scopeTerm->IsValid()); + + rv = scopeTerm->InitializeAdapter(m_termList); + } + + return rv; +} + +nsresult nsMsgSearchSession::BeginSearching() { + // Here's a sloppy way to start the URL, but I don't really have time to + // unify the scheduling mechanisms. If the first scope is a newsgroup, and + // it's not Dredd-capable, we build the URL queue. All other searches can be + // done with one URL + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak)); + if (msgWindow) msgWindow->SetStopped(false); + return DoNextSearch(); +} + +nsresult nsMsgSearchSession::DoNextSearch() { + nsMsgSearchScopeTerm* scope = m_scopeList.ElementAt(m_idxRunningScope); + if (scope->m_attribute == nsMsgSearchScope::onlineMail || + (scope->m_attribute == nsMsgSearchScope::news && scope->m_searchServer)) { + if (scope->m_adapter) { + m_runningUrl.Truncate(); + scope->m_adapter->GetEncoding(getter_Copies(m_runningUrl)); + } + NS_ENSURE_STATE(!m_runningUrl.IsEmpty()); + return GetNextUrl(); + } else { + return SearchWOUrls(); + } +} + +nsresult nsMsgSearchSession::GetNextUrl() { + nsCOMPtr<nsIMsgMessageService> msgService; + + bool stopped = false; + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak)); + if (msgWindow) msgWindow->GetStopped(&stopped); + if (stopped) return NS_OK; + + nsMsgSearchScopeTerm* currentTerm = GetRunningScope(); + NS_ENSURE_TRUE(currentTerm, NS_ERROR_NULL_POINTER); + EnableFolderNotifications(false); + nsCOMPtr<nsIMsgFolder> folder = currentTerm->m_folder; + if (folder) { + nsCString folderUri; + folder->GetURI(folderUri); + nsresult rv = + GetMessageServiceFromURI(folderUri, getter_AddRefs(msgService)); + + if (NS_SUCCEEDED(rv) && msgService && currentTerm) + msgService->Search(this, msgWindow, currentTerm->m_folder, m_runningUrl); + return rv; + } + return NS_OK; +} + +/* static */ +void nsMsgSearchSession::TimerCallback(nsITimer* aTimer, void* aClosure) { + NS_ENSURE_TRUE_VOID(aClosure); + nsMsgSearchSession* searchSession = (nsMsgSearchSession*)aClosure; + bool done; + bool stopped = false; + + searchSession->TimeSlice(&done); + nsCOMPtr<nsIMsgWindow> msgWindow( + do_QueryReferent(searchSession->m_msgWindowWeak)); + if (msgWindow) msgWindow->GetStopped(&stopped); + + if (done || stopped) { + if (aTimer) aTimer->Cancel(); + searchSession->m_backgroundTimer = nullptr; + if (searchSession->m_idxRunningScope < searchSession->m_scopeList.Length()) + searchSession->DoNextSearch(); + else + searchSession->NotifyListenersDone(NS_OK); + } +} + +nsresult nsMsgSearchSession::StartTimer() { + nsresult rv; + + m_backgroundTimer = do_CreateInstance("@mozilla.org/timer;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + m_backgroundTimer->InitWithNamedFuncCallback( + TimerCallback, (void*)this, 0, nsITimer::TYPE_REPEATING_SLACK, + "nsMsgSearchSession::TimerCallback"); + TimerCallback(m_backgroundTimer, this); + return NS_OK; +} + +nsresult nsMsgSearchSession::SearchWOUrls() { + EnableFolderNotifications(false); + return StartTimer(); +} + +NS_IMETHODIMP +nsMsgSearchSession::GetRunningAdapter(nsIMsgSearchAdapter** aSearchAdapter) { + NS_ENSURE_ARG_POINTER(aSearchAdapter); + *aSearchAdapter = nullptr; + nsMsgSearchScopeTerm* scope = GetRunningScope(); + if (scope) { + NS_IF_ADDREF(*aSearchAdapter = scope->m_adapter); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchSession::AddSearchHit(nsIMsgDBHdr* aHeader, + nsIMsgFolder* aFolder) { + nsCOMPtr<nsIMsgSearchNotify> listener; + m_iListener = 0; + while (m_iListener != -1 && m_iListener < (signed)m_listenerList.Length()) { + listener = m_listenerList[m_iListener]; + int32_t listenerFlags = m_listenerFlagList[m_iListener++]; + if (!listenerFlags || (listenerFlags & nsIMsgSearchSession::onSearchHit)) + listener->OnSearchHit(aHeader, aFolder); + } + m_iListener = -1; + return NS_OK; +} + +nsresult nsMsgSearchSession::NotifyListenersDone(nsresult aStatus) { + // need to stabilize "this" in case one of the listeners releases the last + // reference to us. + RefPtr<nsIMsgSearchSession> kungFuDeathGrip(this); + + nsCOMPtr<nsIMsgSearchNotify> listener; + m_iListener = 0; + while (m_iListener != -1 && m_iListener < (signed)m_listenerList.Length()) { + listener = m_listenerList[m_iListener]; + int32_t listenerFlags = m_listenerFlagList[m_iListener++]; + if (!listenerFlags || (listenerFlags & nsIMsgSearchSession::onSearchDone)) + listener->OnSearchDone(aStatus); + } + m_iListener = -1; + return NS_OK; +} + +void nsMsgSearchSession::DestroyScopeList() { + nsMsgSearchScopeTerm* scope = nullptr; + + for (int32_t i = m_scopeList.Length() - 1; i >= 0; i--) { + scope = m_scopeList.ElementAt(i); + // NS_ASSERTION (scope->IsValid(), "invalid search scope"); + if (scope->m_adapter) scope->m_adapter->ClearScope(); + } + m_scopeList.Clear(); +} + +void nsMsgSearchSession::DestroyTermList() { m_termList.Clear(); } + +nsMsgSearchScopeTerm* nsMsgSearchSession::GetRunningScope() { + return m_scopeList.SafeElementAt(m_idxRunningScope, nullptr); +} + +nsresult nsMsgSearchSession::TimeSlice(bool* aDone) { + // we only do serial for now. + return TimeSliceSerial(aDone); +} + +void nsMsgSearchSession::ReleaseFolderDBRef() { + nsMsgSearchScopeTerm* scope = GetRunningScope(); + if (!scope) return; + + bool isOpen = false; + uint32_t flags; + nsCOMPtr<nsIMsgFolder> folder; + scope->GetFolder(getter_AddRefs(folder)); + nsCOMPtr<nsIMsgMailSession> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1"); + if (!mailSession || !folder) return; + + mailSession->IsFolderOpenInWindow(folder, &isOpen); + folder->GetFlags(&flags); + + /*we don't null out the db reference for inbox because inbox is like the + "main" folder and performance outweighs footprint */ + if (!isOpen && !(nsMsgFolderFlags::Inbox & flags)) + folder->SetMsgDatabase(nullptr); +} +nsresult nsMsgSearchSession::TimeSliceSerial(bool* aDone) { + // This version of TimeSlice runs each scope term one at a time, and waits + // until one scope term is finished before starting another one. When we're + // searching the local disk, this is the fastest way to do it. + + NS_ENSURE_ARG_POINTER(aDone); + + nsMsgSearchScopeTerm* scope = GetRunningScope(); + if (!scope) { + *aDone = true; + return NS_OK; + } + + nsresult rv = scope->TimeSlice(aDone); + if (*aDone || NS_FAILED(rv)) { + EnableFolderNotifications(true); + ReleaseFolderDBRef(); + m_idxRunningScope++; + EnableFolderNotifications(false); + // check if the next scope is an online search; if so, + // set *aDone to true so that we'll try to run the next + // search in TimerCallback. + scope = GetRunningScope(); + if (scope && (scope->m_attribute == nsMsgSearchScope::onlineMail || + (scope->m_attribute == nsMsgSearchScope::news && + scope->m_searchServer))) { + *aDone = true; + return rv; + } + } + *aDone = false; + return rv; +} + +void nsMsgSearchSession::EnableFolderNotifications(bool aEnable) { + nsMsgSearchScopeTerm* scope = GetRunningScope(); + if (scope) { + nsCOMPtr<nsIMsgFolder> folder; + scope->GetFolder(getter_AddRefs(folder)); + if (folder) // enable msg count notifications + folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications, + aEnable); + } +} + +// this method is used for adding new hdrs to quick search view +NS_IMETHODIMP +nsMsgSearchSession::MatchHdr(nsIMsgDBHdr* aMsgHdr, nsIMsgDatabase* aDatabase, + bool* aResult) { + nsMsgSearchScopeTerm* scope = m_scopeList.SafeElementAt(0, nullptr); + if (scope) { + if (!scope->m_adapter) scope->InitializeAdapter(m_termList); + if (scope->m_adapter) { + nsAutoString nullCharset, folderCharset; + scope->m_adapter->GetSearchCharsets(nullCharset, folderCharset); + NS_ConvertUTF16toUTF8 charset(folderCharset.get()); + nsMsgSearchOfflineMail::MatchTermsForSearch( + aMsgHdr, m_termList, charset.get(), scope, aDatabase, + &m_expressionTree, aResult); + } + } + return NS_OK; +} diff --git a/comm/mailnews/search/src/nsMsgSearchSession.h b/comm/mailnews/search/src/nsMsgSearchSession.h new file mode 100644 index 0000000000..a5f311e1e5 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgSearchSession.h @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgSearchSession_h___ +#define nsMsgSearchSession_h___ + +#include "nscore.h" +#include "nsMsgSearchCore.h" +#include "nsIMsgSearchSession.h" +#include "nsIUrlListener.h" +#include "nsITimer.h" +#include "nsWeakReference.h" + +class nsMsgSearchAdapter; +class nsMsgSearchBoolExpression; +class nsMsgSearchScopeTerm; + +class nsMsgSearchSession : public nsIMsgSearchSession, + public nsIUrlListener, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGSEARCHSESSION + NS_DECL_NSIURLLISTENER + + nsMsgSearchSession(); + + protected: + virtual ~nsMsgSearchSession(); + + nsWeakPtr m_msgWindowWeak; + nsresult Initialize(); + nsresult StartTimer(); + nsresult TimeSlice(bool* aDone); + nsMsgSearchScopeTerm* GetRunningScope(); + void StopRunning(); + nsresult BeginSearching(); + nsresult DoNextSearch(); + nsresult SearchWOUrls(); + nsresult GetNextUrl(); + nsresult NotifyListenersDone(nsresult status); + void EnableFolderNotifications(bool aEnable); + void ReleaseFolderDBRef(); + + nsTArray<RefPtr<nsMsgSearchScopeTerm>> m_scopeList; + nsTArray<RefPtr<nsIMsgSearchTerm>> m_termList; + + nsTArray<nsCOMPtr<nsIMsgSearchNotify>> m_listenerList; + nsTArray<int32_t> m_listenerFlagList; + /** + * Iterator index for m_listenerList/m_listenerFlagList. We used to use an + * nsTObserverArray for m_listenerList but its auto-adjusting iterator was + * not helping us keep our m_listenerFlagList iterator correct. + * + * We are making the simplifying assumption that our notifications are + * non-reentrant. In the exceptional case that it turns out they are + * reentrant, we assume that this is the result of canceling a search while + * the session is active and initiating a new one. In that case, we assume + * the outer iteration can safely be abandoned. + * + * This value is defined to be the index of the next listener we will process. + * This allows us to use the sentinel value of -1 to convey that no iteration + * is in progress (and the iteration process to abort if the value transitions + * to -1, which we always set on conclusion of our loop). + */ + int32_t m_iListener; + + void DestroyTermList(); + void DestroyScopeList(); + + static void TimerCallback(nsITimer* aTimer, void* aClosure); + // support for searching multiple scopes in serial + nsresult TimeSliceSerial(bool* aDone); + nsresult TimeSliceParallel(); + + nsMsgSearchAttribValue m_sortAttribute; + uint32_t m_idxRunningScope; + bool m_handlingError; + nsCString m_runningUrl; // The url for the current search + nsCOMPtr<nsITimer> m_backgroundTimer; + bool m_searchPaused; + nsMsgSearchBoolExpression* m_expressionTree; +}; + +#endif diff --git a/comm/mailnews/search/src/nsMsgSearchTerm.cpp b/comm/mailnews/search/src/nsMsgSearchTerm.cpp new file mode 100644 index 0000000000..7aad1628e5 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgSearchTerm.cpp @@ -0,0 +1,1797 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "prmem.h" +#include "nsMsgSearchCore.h" +#include "nsIMsgSearchSession.h" +#include "nsMsgUtils.h" +#include "nsIMsgDatabase.h" +#include "nsIMsgHdr.h" +#include "nsMsgSearchTerm.h" +#include "nsMsgSearchScopeTerm.h" +#include "nsMsgBodyHandler.h" +#include "nsMsgResultElement.h" +#include "nsIMsgImapMailFolder.h" +#include "nsMsgSearchImap.h" +#include "nsMsgLocalSearch.h" +#include "nsMsgSearchNews.h" +#include "nsMsgSearchValue.h" +#include "nsMsgI18N.h" +#include "nsIMimeConverter.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsIMsgFilterPlugin.h" +#include "nsIFile.h" +#include "nsISeekableStream.h" +#include "nsNetCID.h" +#include "nsIFileStreams.h" +#include "nsUnicharUtils.h" +#include "nsIAbCard.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include <ctype.h> +#include "nsIMsgTagService.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgFilterService.h" +#include "nsIMsgPluggableStore.h" +#include "nsIAbManager.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/mailnews/MimeHeaderParser.h" +#include "mozilla/Utf8.h" + +using namespace mozilla::mailnews; + +//--------------------------------------------------------------------------- +// nsMsgSearchTerm specifies one criterion, e.g. name contains phil +//--------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +//-------------------- Implementation of nsMsgSearchTerm ----------------------- +//----------------------------------------------------------------------------- +#define MAILNEWS_CUSTOM_HEADERS "mailnews.customHeaders" + +typedef struct { + nsMsgSearchAttribValue attrib; + const char* attribName; +} nsMsgSearchAttribEntry; + +nsMsgSearchAttribEntry SearchAttribEntryTable[] = { + {nsMsgSearchAttrib::Subject, "subject"}, + {nsMsgSearchAttrib::Sender, "from"}, + {nsMsgSearchAttrib::Body, "body"}, + {nsMsgSearchAttrib::Date, "date"}, + {nsMsgSearchAttrib::Priority, "priority"}, + {nsMsgSearchAttrib::MsgStatus, "status"}, + {nsMsgSearchAttrib::To, "to"}, + {nsMsgSearchAttrib::CC, "cc"}, + {nsMsgSearchAttrib::ToOrCC, "to or cc"}, + {nsMsgSearchAttrib::AllAddresses, "all addresses"}, + {nsMsgSearchAttrib::AgeInDays, "age in days"}, + {nsMsgSearchAttrib::Keywords, "tag"}, + {nsMsgSearchAttrib::Size, "size"}, + // this used to be nsMsgSearchAttrib::SenderInAddressBook + // we used to have two Sender menuitems + // for backward compatibility, we can still parse + // the old style. see bug #179803 + {nsMsgSearchAttrib::Sender, "from in ab"}, + {nsMsgSearchAttrib::JunkStatus, "junk status"}, + {nsMsgSearchAttrib::JunkPercent, "junk percent"}, + {nsMsgSearchAttrib::JunkScoreOrigin, "junk score origin"}, + {nsMsgSearchAttrib::HasAttachmentStatus, "has attachment status"}, +}; + +static const unsigned int sNumSearchAttribEntryTable = + MOZ_ARRAY_LENGTH(SearchAttribEntryTable); + +// Take a string which starts off with an attribute +// and return the matching attribute. If the string is not in the table, and it +// begins with a quote, then we can conclude that it is an arbitrary header. +// Otherwise if not in the table, it is the id for a custom search term. +nsresult NS_MsgGetAttributeFromString(const char* string, + nsMsgSearchAttribValue* attrib, + nsACString& aCustomId) { + NS_ENSURE_ARG_POINTER(string); + NS_ENSURE_ARG_POINTER(attrib); + + bool found = false; + bool isArbitraryHeader = false; + // arbitrary headers have a leading quote + if (*string != '"') { + for (unsigned int idxAttrib = 0; idxAttrib < sNumSearchAttribEntryTable; + idxAttrib++) { + if (!PL_strcasecmp(string, + SearchAttribEntryTable[idxAttrib].attribName)) { + found = true; + *attrib = SearchAttribEntryTable[idxAttrib].attrib; + break; + } + } + } else // remove the leading quote + { + string++; + isArbitraryHeader = true; + } + + if (!found && !isArbitraryHeader) { + // must be a custom attribute + *attrib = nsMsgSearchAttrib::Custom; + aCustomId.Assign(string); + return NS_OK; + } + + if (!found) { + nsresult rv; + bool goodHdr; + IsRFC822HeaderFieldName(string, &goodHdr); + if (!goodHdr) return NS_MSG_INVALID_CUSTOM_HEADER; + // 49 is for showing customize... in ui, headers start from 50 onwards up + // until 99. + *attrib = nsMsgSearchAttrib::OtherHeader + 1; + + nsCOMPtr<nsIPrefService> prefService = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefBranch> prefBranch; + rv = prefService->GetBranch(nullptr, getter_AddRefs(prefBranch)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString headers; + prefBranch->GetCharPref(MAILNEWS_CUSTOM_HEADERS, headers); + + if (!headers.IsEmpty()) { + nsAutoCString hdrStr(headers); + hdrStr.StripWhitespace(); // remove whitespace before parsing + + char* newStr = hdrStr.BeginWriting(); + char* token = NS_strtok(":", &newStr); + uint32_t i = 0; + while (token) { + if (PL_strcasecmp(token, string) == 0) { + *attrib += i; // we found custom header in the pref + found = true; + break; + } + token = NS_strtok(":", &newStr); + i++; + } + } + } + // If we didn't find the header in MAILNEWS_CUSTOM_HEADERS, we're + // going to return NS_OK and an attrib of nsMsgSearchAttrib::OtherHeader+1. + // in case it's a client side spam filter description filter, + // which doesn't add its headers to mailnews.customMailHeaders. + // We've already checked that it's a valid header and returned + // an error if so. + + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchTerm::GetAttributeFromString( + const char* aString, nsMsgSearchAttribValue* aAttrib) { + nsAutoCString customId; + return NS_MsgGetAttributeFromString(aString, aAttrib, customId); +} + +nsresult NS_MsgGetStringForAttribute(int16_t attrib, const char** string) { + NS_ENSURE_ARG_POINTER(string); + + bool found = false; + for (unsigned int idxAttrib = 0; idxAttrib < sNumSearchAttribEntryTable; + idxAttrib++) { + // I'm using the idx's as aliases into MSG_SearchAttribute and + // MSG_SearchOperator enums which is legal because of the way the + // enums are defined (starts at 0, numItems at end) + if (attrib == SearchAttribEntryTable[idxAttrib].attrib) { + found = true; + *string = SearchAttribEntryTable[idxAttrib].attribName; + break; + } + } + if (!found) *string = ""; // don't leave the string uninitialized + + // we no longer return invalid attribute. If we cannot find the string in the + // table, then it is an arbitrary header. Return success regardless if found + // or not + return NS_OK; +} + +typedef struct { + nsMsgSearchOpValue op; + const char* opName; +} nsMsgSearchOperatorEntry; + +nsMsgSearchOperatorEntry SearchOperatorEntryTable[] = { + {nsMsgSearchOp::Contains, "contains"}, + {nsMsgSearchOp::DoesntContain, "doesn't contain"}, + {nsMsgSearchOp::Is, "is"}, + {nsMsgSearchOp::Isnt, "isn't"}, + {nsMsgSearchOp::IsEmpty, "is empty"}, + {nsMsgSearchOp::IsntEmpty, "isn't empty"}, + {nsMsgSearchOp::IsBefore, "is before"}, + {nsMsgSearchOp::IsAfter, "is after"}, + {nsMsgSearchOp::IsHigherThan, "is higher than"}, + {nsMsgSearchOp::IsLowerThan, "is lower than"}, + {nsMsgSearchOp::BeginsWith, "begins with"}, + {nsMsgSearchOp::EndsWith, "ends with"}, + {nsMsgSearchOp::IsInAB, "is in ab"}, + {nsMsgSearchOp::IsntInAB, "isn't in ab"}, + {nsMsgSearchOp::IsGreaterThan, "is greater than"}, + {nsMsgSearchOp::IsLessThan, "is less than"}, + {nsMsgSearchOp::Matches, "matches"}, + {nsMsgSearchOp::DoesntMatch, "doesn't match"}}; + +static const unsigned int sNumSearchOperatorEntryTable = + MOZ_ARRAY_LENGTH(SearchOperatorEntryTable); + +nsresult NS_MsgGetOperatorFromString(const char* string, int16_t* op) { + NS_ENSURE_ARG_POINTER(string); + NS_ENSURE_ARG_POINTER(op); + + bool found = false; + for (unsigned int idxOp = 0; idxOp < sNumSearchOperatorEntryTable; idxOp++) { + // I'm using the idx's as aliases into MSG_SearchAttribute and + // MSG_SearchOperator enums which is legal because of the way the + // enums are defined (starts at 0, numItems at end) + if (!PL_strcasecmp(string, SearchOperatorEntryTable[idxOp].opName)) { + found = true; + *op = SearchOperatorEntryTable[idxOp].op; + break; + } + } + return (found) ? NS_OK : NS_ERROR_INVALID_ARG; +} + +nsresult NS_MsgGetStringForOperator(int16_t op, const char** string) { + NS_ENSURE_ARG_POINTER(string); + + bool found = false; + for (unsigned int idxOp = 0; idxOp < sNumSearchOperatorEntryTable; idxOp++) { + // I'm using the idx's as aliases into MSG_SearchAttribute and + // MSG_SearchOperator enums which is legal because of the way the + // enums are defined (starts at 0, numItems at end) + if (op == SearchOperatorEntryTable[idxOp].op) { + found = true; + *string = SearchOperatorEntryTable[idxOp].opName; + break; + } + } + + return (found) ? NS_OK : NS_ERROR_INVALID_ARG; +} + +void NS_MsgGetUntranslatedStatusName(uint32_t s, nsCString* outName) { + const char* tmpOutName = NULL; +#define MSG_STATUS_MASK \ + (nsMsgMessageFlags::Read | nsMsgMessageFlags::Replied | \ + nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::New | \ + nsMsgMessageFlags::Marked) + uint32_t maskOut = (s & MSG_STATUS_MASK); + + // diddle the flags to pay attention to the most important ones first, if + // multiple flags are set. Should remove this code from the winfe. + if (maskOut & nsMsgMessageFlags::New) maskOut = nsMsgMessageFlags::New; + if (maskOut & nsMsgMessageFlags::Replied && + maskOut & nsMsgMessageFlags::Forwarded) + maskOut = nsMsgMessageFlags::Replied | nsMsgMessageFlags::Forwarded; + else if (maskOut & nsMsgMessageFlags::Forwarded) + maskOut = nsMsgMessageFlags::Forwarded; + else if (maskOut & nsMsgMessageFlags::Replied) + maskOut = nsMsgMessageFlags::Replied; + + switch (maskOut) { + case nsMsgMessageFlags::Read: + tmpOutName = "read"; + break; + case nsMsgMessageFlags::Replied: + tmpOutName = "replied"; + break; + case nsMsgMessageFlags::Forwarded: + tmpOutName = "forwarded"; + break; + case nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::Replied: + tmpOutName = "replied and forwarded"; + break; + case nsMsgMessageFlags::New: + tmpOutName = "new"; + break; + case nsMsgMessageFlags::Marked: + tmpOutName = "flagged"; + break; + default: + // This is fine, status may be "unread" for example + break; + } + + if (tmpOutName) *outName = tmpOutName; +} + +int32_t NS_MsgGetStatusValueFromName(char* name) { + if (!strcmp("read", name)) return nsMsgMessageFlags::Read; + if (!strcmp("replied", name)) return nsMsgMessageFlags::Replied; + if (!strcmp("forwarded", name)) return nsMsgMessageFlags::Forwarded; + if (!strcmp("replied and forwarded", name)) + return nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::Replied; + if (!strcmp("new", name)) return nsMsgMessageFlags::New; + if (!strcmp("flagged", name)) return nsMsgMessageFlags::Marked; + return 0; +} + +// Needed for DeStream method. +nsMsgSearchTerm::nsMsgSearchTerm() { + // initialize this to zero + m_value.attribute = 0; + m_value.u.priority = 0; + m_attribute = nsMsgSearchAttrib::Default; + m_operator = nsMsgSearchOp::Contains; + mBeginsGrouping = false; + mEndsGrouping = false; + m_matchAll = false; + + // valgrind warning during GC/java data check suggests + // m_booleanp needs to be initialized too. + m_booleanOp = nsMsgSearchBooleanOp::BooleanAND; +} + +nsMsgSearchTerm::nsMsgSearchTerm(nsMsgSearchAttribValue attrib, + nsMsgSearchOpValue op, nsIMsgSearchValue* val, + nsMsgSearchBooleanOperator boolOp, + const char* aCustomString) { + m_operator = op; + m_attribute = attrib; + m_booleanOp = boolOp; + if (attrib > nsMsgSearchAttrib::OtherHeader && + attrib < nsMsgSearchAttrib::kNumMsgSearchAttributes && aCustomString) { + m_arbitraryHeader = aCustomString; + ToLowerCaseExceptSpecials(m_arbitraryHeader); + } else if (attrib == nsMsgSearchAttrib::Custom) { + m_customId = aCustomString; + } + + nsMsgResultElement::AssignValues(val, &m_value); + m_matchAll = false; + mBeginsGrouping = false; + mEndsGrouping = false; +} + +nsMsgSearchTerm::~nsMsgSearchTerm() {} + +NS_IMPL_ISUPPORTS(nsMsgSearchTerm, nsIMsgSearchTerm) + +// Perhaps we could find a better place for this? +// Caller needs to free. +/* static */ char* nsMsgSearchTerm::EscapeQuotesInStr(const char* str) { + int numQuotes = 0; + for (const char* strPtr = str; *strPtr; strPtr++) + if (*strPtr == '"') numQuotes++; + int escapedStrLen = PL_strlen(str) + numQuotes; + char* escapedStr = (char*)PR_Malloc(escapedStrLen + 1); + if (escapedStr) { + char* destPtr; + for (destPtr = escapedStr; *str; str++) { + if (*str == '"') *destPtr++ = '\\'; + *destPtr++ = *str; + } + *destPtr = '\0'; + } + return escapedStr; +} + +nsresult nsMsgSearchTerm::OutputValue(nsCString& outputStr) { + if (IS_STRING_ATTRIBUTE(m_attribute) && !m_value.utf8String.IsEmpty()) { + bool quoteVal = false; + // need to quote strings with ')' and strings starting with '"' or ' ' + // filter code will escape quotes + if (m_value.utf8String.FindChar(')') != kNotFound || + (m_value.utf8String.First() == ' ') || + (m_value.utf8String.First() == '"')) { + quoteVal = true; + outputStr += "\""; + } + if (m_value.utf8String.FindChar('"') != kNotFound) { + char* escapedString = + nsMsgSearchTerm::EscapeQuotesInStr(m_value.utf8String.get()); + if (escapedString) { + outputStr += escapedString; + PR_Free(escapedString); + } + + } else { + outputStr += m_value.utf8String; + } + if (quoteVal) outputStr += "\""; + } else { + switch (m_attribute) { + case nsMsgSearchAttrib::Date: { + PRExplodedTime exploded; + PR_ExplodeTime(m_value.u.date, PR_LocalTimeParameters, &exploded); + + // wow, so tm_mon is 0 based, tm_mday is 1 based. + char dateBuf[100]; + PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%d-%b-%Y", &exploded); + outputStr += dateBuf; + break; + } + case nsMsgSearchAttrib::AgeInDays: { + outputStr.AppendInt(m_value.u.age); + break; + } + case nsMsgSearchAttrib::JunkStatus: { + outputStr.AppendInt( + m_value.u.junkStatus); // only if we write to disk, right? + break; + } + case nsMsgSearchAttrib::JunkPercent: { + outputStr.AppendInt(m_value.u.junkPercent); + break; + } + case nsMsgSearchAttrib::MsgStatus: { + nsAutoCString status; + NS_MsgGetUntranslatedStatusName(m_value.u.msgStatus, &status); + outputStr += status; + break; + } + case nsMsgSearchAttrib::Priority: { + nsAutoCString priority; + NS_MsgGetUntranslatedPriorityName(m_value.u.priority, priority); + outputStr += priority; + break; + } + case nsMsgSearchAttrib::HasAttachmentStatus: { + outputStr.AppendLiteral("true"); // don't need anything here, really + break; + } + case nsMsgSearchAttrib::Size: { + outputStr.AppendInt(m_value.u.size); + break; + } + case nsMsgSearchAttrib::Uint32HdrProperty: { + outputStr.AppendInt(m_value.u.msgStatus); + break; + } + default: + NS_ASSERTION(false, "trying to output invalid attribute"); + break; + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchTerm::GetTermAsString(nsACString& outStream) { + const char* operatorStr; + nsAutoCString outputStr; + nsresult rv; + + if (m_matchAll) { + outStream = "ALL"; + return NS_OK; + } + + // if arbitrary header, use it instead! + if (m_attribute > nsMsgSearchAttrib::OtherHeader && + m_attribute < nsMsgSearchAttrib::kNumMsgSearchAttributes) { + outputStr = "\""; + outputStr += m_arbitraryHeader; + outputStr += "\""; + } else if (m_attribute == nsMsgSearchAttrib::Custom) { + // use the custom id as the string + outputStr = m_customId; + } + + else if (m_attribute == nsMsgSearchAttrib::Uint32HdrProperty) { + outputStr = m_hdrProperty; + } else { + const char* attrib; + rv = NS_MsgGetStringForAttribute(m_attribute, &attrib); + NS_ENSURE_SUCCESS(rv, rv); + + outputStr = attrib; + } + + outputStr += ','; + + rv = NS_MsgGetStringForOperator(m_operator, &operatorStr); + NS_ENSURE_SUCCESS(rv, rv); + + outputStr += operatorStr; + outputStr += ','; + + OutputValue(outputStr); + outStream = outputStr; + return NS_OK; +} + +// fill in m_value from the input stream. +nsresult nsMsgSearchTerm::ParseValue(char* inStream) { + if (IS_STRING_ATTRIBUTE(m_attribute)) { + bool quoteVal = false; + while (isspace(*inStream)) inStream++; + // need to remove pair of '"', if present + if (*inStream == '"') { + quoteVal = true; + inStream++; + } + int valueLen = PL_strlen(inStream); + if (quoteVal && inStream[valueLen - 1] == '"') valueLen--; + + m_value.utf8String.Assign(inStream, valueLen); + CopyUTF8toUTF16(m_value.utf8String, m_value.utf16String); + } else { + switch (m_attribute) { + case nsMsgSearchAttrib::Date: + PR_ParseTimeString(inStream, false, &m_value.u.date); + break; + case nsMsgSearchAttrib::MsgStatus: + m_value.u.msgStatus = NS_MsgGetStatusValueFromName(inStream); + break; + case nsMsgSearchAttrib::Priority: + NS_MsgGetPriorityFromString(inStream, m_value.u.priority); + break; + case nsMsgSearchAttrib::AgeInDays: + m_value.u.age = atoi(inStream); + break; + case nsMsgSearchAttrib::JunkStatus: + m_value.u.junkStatus = + atoi(inStream); // only if we read from disk, right? + break; + case nsMsgSearchAttrib::JunkPercent: + m_value.u.junkPercent = atoi(inStream); + break; + case nsMsgSearchAttrib::HasAttachmentStatus: + m_value.u.msgStatus = nsMsgMessageFlags::Attachment; + break; // this should always be true. + case nsMsgSearchAttrib::Size: + m_value.u.size = atoi(inStream); + break; + default: + NS_ASSERTION(false, "invalid attribute parsing search term value"); + break; + } + } + m_value.attribute = m_attribute; + return NS_OK; +} + +// find the operator code for this operator string. +nsresult nsMsgSearchTerm::ParseOperator(char* inStream, + nsMsgSearchOpValue* value) { + NS_ENSURE_ARG_POINTER(value); + int16_t operatorVal; + while (isspace(*inStream)) inStream++; + + char* commaSep = PL_strchr(inStream, ','); + + if (commaSep) *commaSep = '\0'; + + nsresult rv = NS_MsgGetOperatorFromString(inStream, &operatorVal); + if (NS_SUCCEEDED(rv)) *value = (nsMsgSearchOpValue)operatorVal; + return rv; +} + +// find the attribute code for this comma-delimited attribute. +nsresult nsMsgSearchTerm::ParseAttribute(char* inStream, + nsMsgSearchAttribValue* attrib) { + while (isspace(*inStream)) inStream++; + + // if we are dealing with an arbitrary header, it will be quoted.... + // it seems like a kludge, but to distinguish arbitrary headers from + // standard headers with the same name, like "Date", we'll use the + // presence of the quote to recognize arbitrary headers. We leave the + // leading quote as a flag, but remove the trailing quote. + bool quoteVal = false; + if (*inStream == '"') quoteVal = true; + + // arbitrary headers are quoted. Skip first character, which will be the + // first quote for arbitrary headers + char* separator = strchr(inStream + 1, quoteVal ? '"' : ','); + + if (separator) *separator = '\0'; + + nsAutoCString customId; + nsresult rv = NS_MsgGetAttributeFromString(inStream, attrib, m_customId); + NS_ENSURE_SUCCESS(rv, rv); + + if (*attrib > nsMsgSearchAttrib::OtherHeader && + *attrib < nsMsgSearchAttrib::kNumMsgSearchAttributes) { + // We are dealing with an arbitrary header. + m_arbitraryHeader = inStream + 1; // remove the leading quote + ToLowerCaseExceptSpecials(m_arbitraryHeader); + } + return rv; +} + +// De stream one search term. If the condition looks like +// condition = "(to or cc, contains, r-thompson) AND (body, doesn't contain, +// fred)" This routine should get called twice, the first time with "to or cc, +// contains, r-thompson", the second time with "body, doesn't contain, fred" + +nsresult nsMsgSearchTerm::DeStreamNew(char* inStream, int16_t /*length*/) { + if (!strcmp(inStream, "ALL")) { + m_matchAll = true; + return NS_OK; + } + char* commaSep = PL_strchr(inStream, ','); + nsresult rv = ParseAttribute( + inStream, + &m_attribute); // will allocate space for arbitrary header if necessary + NS_ENSURE_SUCCESS(rv, rv); + if (!commaSep) return NS_ERROR_INVALID_ARG; + char* secondCommaSep = PL_strchr(commaSep + 1, ','); + if (commaSep) rv = ParseOperator(commaSep + 1, &m_operator); + NS_ENSURE_SUCCESS(rv, rv); + // convert label filters and saved searches to keyword equivalents + if (secondCommaSep) ParseValue(secondCommaSep + 1); + return NS_OK; +} + +// Looks in the MessageDB for the user specified arbitrary header, if it finds +// the header, it then looks for a match against the value for the header. +nsresult nsMsgSearchTerm::MatchArbitraryHeader( + nsIMsgSearchScopeTerm* scope, uint32_t length /* in lines*/, + const char* charset, bool charsetOverride, nsIMsgDBHdr* msg, + nsIMsgDatabase* db, const nsACString& headers, bool ForFiltering, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + *pResult = false; + nsresult rv = NS_OK; + bool matchExpected = m_operator == nsMsgSearchOp::Contains || + m_operator == nsMsgSearchOp::Is || + m_operator == nsMsgSearchOp::BeginsWith || + m_operator == nsMsgSearchOp::EndsWith; + // Initialize result to what we want if we don't find the header at all. + bool result = !matchExpected; + + nsCString dbHdrValue; + msg->GetStringProperty(m_arbitraryHeader.get(), dbHdrValue); + if (!dbHdrValue.IsEmpty()) { + // Match value with the other info. It doesn't check all header occurrences, + // so we use it only if we match and do line by line headers parsing + // otherwise. + rv = MatchRfc2047String(dbHdrValue, charset, charsetOverride, pResult); + if (matchExpected == *pResult) return rv; + + // Preset result in case we don't have access to the headers, like for IMAP. + result = *pResult; + } + + nsMsgBodyHandler* bodyHandler = + new nsMsgBodyHandler(scope, length, msg, db, headers.BeginReading(), + headers.Length(), ForFiltering); + bodyHandler->SetStripHeaders(false); + + nsCString headerFullValue; // Contains matched header value accumulated over + // multiple lines. + nsAutoCString buf; + nsAutoCString curMsgHeader; + bool processingHeaders = true; + + while (processingHeaders) { + nsCString charsetIgnored; + if (bodyHandler->GetNextLine(buf, charsetIgnored) < 0 || + EMPTY_MESSAGE_LINE(buf)) + processingHeaders = + false; // No more lines or empty line terminating headers. + + bool isContinuationHeader = + processingHeaders ? NS_IsAsciiWhitespace(buf.CharAt(0)) : false; + + // If we're not on a continuation header the header value is not empty, + // we have finished accumulating the header value by iterating over all + // header lines. Now we need to check whether the value is a match. + if (!isContinuationHeader && !headerFullValue.IsEmpty()) { + bool stringMatches; + // Match value with the other info. + rv = MatchRfc2047String(headerFullValue, charset, charsetOverride, + &stringMatches); + if (matchExpected == stringMatches) // if we found a match + { + // If we found a match, stop examining the headers. + processingHeaders = false; + result = stringMatches; + } + // Prepare for repeated header of the same type. + headerFullValue.Truncate(); + } + + // We got result or finished processing all lines. + if (!processingHeaders) break; + + char* buf_end = (char*)(buf.get() + buf.Length()); + int headerLength = m_arbitraryHeader.Length(); + + // If the line starts with whitespace, then we use the current header. + if (!isContinuationHeader) { + // Here we start a new header. + uint32_t colonPos = buf.FindChar(':'); + curMsgHeader = StringHead(buf, colonPos); + } + + if (curMsgHeader.Equals(m_arbitraryHeader, + nsCaseInsensitiveCStringComparator)) { + // Process the value: + // Value occurs after the header name or whitespace continuation char. + const char* headerValue = + buf.get() + (isContinuationHeader ? 1 : headerLength); + if (headerValue < buf_end && + headerValue[0] == + ':') // + 1 to account for the colon which is MANDATORY + headerValue++; + + // Strip leading white space. + while (headerValue < buf_end && isspace(*headerValue)) headerValue++; + + // Strip trailing white space. + char* end = buf_end - 1; + while (headerValue < end && isspace(*end)) { + *end = '\0'; + end--; + } + + // Any continuation whitespace is converted to a single space. + if (!headerFullValue.IsEmpty()) headerFullValue.Append(' '); + headerFullValue.Append(nsDependentCString(headerValue)); + } + } + + delete bodyHandler; + *pResult = result; + return rv; +} + +NS_IMETHODIMP nsMsgSearchTerm::MatchHdrProperty(nsIMsgDBHdr* aHdr, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_ENSURE_ARG_POINTER(aHdr); + + nsCString dbHdrValue; + aHdr->GetStringProperty(m_hdrProperty.get(), dbHdrValue); + return MatchString(dbHdrValue, nullptr, aResult); +} + +NS_IMETHODIMP nsMsgSearchTerm::MatchFolderFlag(nsIMsgDBHdr* aMsgToMatch, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aMsgToMatch); + NS_ENSURE_ARG_POINTER(aResult); + + nsCOMPtr<nsIMsgFolder> msgFolder; + nsresult rv = aMsgToMatch->GetFolder(getter_AddRefs(msgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t folderFlags; + msgFolder->GetFlags(&folderFlags); + return MatchStatus(folderFlags, aResult); +} + +NS_IMETHODIMP nsMsgSearchTerm::MatchUint32HdrProperty(nsIMsgDBHdr* aHdr, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_ENSURE_ARG_POINTER(aHdr); + + nsresult rv = NS_OK; + uint32_t dbHdrValue; + aHdr->GetUint32Property(m_hdrProperty.get(), &dbHdrValue); + + bool result = false; + switch (m_operator) { + case nsMsgSearchOp::IsGreaterThan: + if (dbHdrValue > m_value.u.msgStatus) result = true; + break; + case nsMsgSearchOp::IsLessThan: + if (dbHdrValue < m_value.u.msgStatus) result = true; + break; + case nsMsgSearchOp::Is: + if (dbHdrValue == m_value.u.msgStatus) result = true; + break; + case nsMsgSearchOp::Isnt: + if (dbHdrValue != m_value.u.msgStatus) result = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for uint"); + } + *aResult = result; + return rv; +} + +nsresult nsMsgSearchTerm::MatchBody(nsIMsgSearchScopeTerm* scope, + uint64_t offset, + uint32_t length /*in lines*/, + const char* folderCharset, nsIMsgDBHdr* msg, + nsIMsgDatabase* db, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv = NS_OK; + + bool result = false; + *pResult = false; + + // Small hack so we don't look all through a message when someone has + // specified "BODY IS foo". ### Since length is in lines, this is not quite + // right. + if ((length > 0) && + (m_operator == nsMsgSearchOp::Is || m_operator == nsMsgSearchOp::Isnt)) + length = m_value.utf8String.Length(); + + nsMsgBodyHandler* bodyHan = new nsMsgBodyHandler(scope, length, msg, db); + if (!bodyHan) return NS_ERROR_OUT_OF_MEMORY; + + nsAutoCString buf; + bool endOfFile = false; // if retValue == 0, we've hit the end of the file + + // Change the sense of the loop so we don't bail out prematurely + // on negative terms. i.e. opDoesntContain must look at all lines + bool boolContinueLoop; + GetMatchAllBeforeDeciding(&boolContinueLoop); + result = boolContinueLoop; + + nsCString compare; + nsCString charset; + while (!endOfFile && result == boolContinueLoop) { + if (bodyHan->GetNextLine(buf, charset) >= 0) { + bool softLineBreak = false; + // Do in-place decoding of quoted printable + if (bodyHan->IsQP()) { + softLineBreak = StringEndsWith(buf, "="_ns); + MsgStripQuotedPrintable(buf); + // If soft line break, chop off the last char as well. + size_t bufLength = buf.Length(); + if ((bufLength > 0) && softLineBreak) buf.SetLength(bufLength - 1); + } + compare.Append(buf); + // If this line ends with a soft line break, loop around + // and get the next line before looking for the search string. + // This assumes the message can't end on a QP soft line break. + // That seems like a pretty safe assumption. + if (softLineBreak) continue; + if (!compare.IsEmpty()) { + char startChar = (char)compare.CharAt(0); + if (startChar != '\r' && startChar != '\n') { + rv = MatchString(compare, + charset.IsEmpty() ? folderCharset : charset.get(), + &result); + } + compare.Truncate(); + } + } else + endOfFile = true; + } + + delete bodyHan; + *pResult = result; + return rv; +} + +nsresult nsMsgSearchTerm::InitializeAddressBook() { + // the search attribute value has the URI for the address book we need to + // load. we need both the database and the directory. + nsresult rv = NS_OK; + + if (mDirectory) { + nsCString uri; + rv = mDirectory->GetURI(uri); + NS_ENSURE_SUCCESS(rv, rv); + + if (!uri.Equals(m_value.utf8String)) + // clear out the directory....we are no longer pointing to the right one + mDirectory = nullptr; + } + if (!mDirectory) { + nsCOMPtr<nsIAbManager> abManager = + do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = + abManager->GetDirectory(m_value.utf8String, getter_AddRefs(mDirectory)); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult nsMsgSearchTerm::MatchInAddressBook(const nsAString& aAddress, + bool* pResult) { + nsresult rv = InitializeAddressBook(); + *pResult = false; + + // Some junkmails have empty From: fields. + if (aAddress.IsEmpty()) return rv; + + if (mDirectory) { + nsCOMPtr<nsIAbCard> cardForAddress = nullptr; + rv = mDirectory->CardForEmailAddress(NS_ConvertUTF16toUTF8(aAddress), + getter_AddRefs(cardForAddress)); + if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) return rv; + switch (m_operator) { + case nsMsgSearchOp::IsInAB: + if (cardForAddress) *pResult = true; + break; + case nsMsgSearchOp::IsntInAB: + if (!cardForAddress) *pResult = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for address book"); + } + } + + return rv; +} + +// *pResult is false when strings don't match, true if they do. +nsresult nsMsgSearchTerm::MatchRfc2047String(const nsACString& rfc2047string, + const char* charset, + bool charsetOverride, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv; + nsCOMPtr<nsIMimeConverter> mimeConverter = + do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoString stringToMatch; + rv = mimeConverter->DecodeMimeHeader(PromiseFlatCString(rfc2047string).get(), + charset, charsetOverride, false, + stringToMatch); + NS_ENSURE_SUCCESS(rv, rv); + if (m_operator == nsMsgSearchOp::IsInAB || + m_operator == nsMsgSearchOp::IsntInAB) + return MatchInAddressBook(stringToMatch, pResult); + + return MatchString(stringToMatch, pResult); +} + +// *pResult is false when strings don't match, true if they do. +nsresult nsMsgSearchTerm::MatchString(const nsACString& stringToMatch, + const char* charset, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + bool result = false; + + nsresult rv = NS_OK; + + // Save some performance for opIsEmpty / opIsntEmpty + if (nsMsgSearchOp::IsEmpty == m_operator) { + if (stringToMatch.IsEmpty()) result = true; + } else if (nsMsgSearchOp::IsntEmpty == m_operator) { + if (!stringToMatch.IsEmpty()) result = true; + } else { + nsAutoString utf16StrToMatch; + rv = NS_ERROR_UNEXPECTED; + if (charset) { + rv = nsMsgI18NConvertToUnicode(nsDependentCString(charset), stringToMatch, + utf16StrToMatch); + } + if (NS_FAILED(rv)) { + // No charset or conversion failed, maybe due to a bad charset, try UTF-8. + if (mozilla::IsUtf8(stringToMatch)) { + CopyUTF8toUTF16(stringToMatch, utf16StrToMatch); + } else { + // Bad luck, let's assume ASCII/windows-1252 then. + CopyASCIItoUTF16(stringToMatch, utf16StrToMatch); + } + } + + rv = MatchString(utf16StrToMatch, &result); + } + + *pResult = result; + return rv; +} + +// *pResult is false when strings don't match, true if they do. +nsresult nsMsgSearchTerm::MatchString(const nsAString& utf16StrToMatch, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + bool result = false; + + nsresult rv = NS_OK; + auto needle = m_value.utf16String; + + switch (m_operator) { + case nsMsgSearchOp::Contains: + if (CaseInsensitiveFindInReadable(needle, utf16StrToMatch)) result = true; + break; + case nsMsgSearchOp::DoesntContain: + if (!CaseInsensitiveFindInReadable(needle, utf16StrToMatch)) + result = true; + break; + case nsMsgSearchOp::Is: + if (needle.Equals(utf16StrToMatch, nsCaseInsensitiveStringComparator)) + result = true; + break; + case nsMsgSearchOp::Isnt: + if (!needle.Equals(utf16StrToMatch, nsCaseInsensitiveStringComparator)) + result = true; + break; + case nsMsgSearchOp::IsEmpty: + if (utf16StrToMatch.IsEmpty()) result = true; + break; + case nsMsgSearchOp::IsntEmpty: + if (!utf16StrToMatch.IsEmpty()) result = true; + break; + case nsMsgSearchOp::BeginsWith: + if (StringBeginsWith(utf16StrToMatch, needle, + nsCaseInsensitiveStringComparator)) + result = true; + break; + case nsMsgSearchOp::EndsWith: + if (StringEndsWith(utf16StrToMatch, needle, + nsCaseInsensitiveStringComparator)) + result = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for matching search results"); + } + + *pResult = result; + return rv; +} + +NS_IMETHODIMP nsMsgSearchTerm::GetMatchAllBeforeDeciding(bool* aResult) { + *aResult = (m_operator == nsMsgSearchOp::DoesntContain || + m_operator == nsMsgSearchOp::Isnt); + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchTerm::MatchRfc822String(const nsACString& string, + const char* charset, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + *pResult = false; + bool result; + + // Change the sense of the loop so we don't bail out prematurely + // on negative terms. i.e. opDoesntContain must look at all recipients + bool boolContinueLoop; + GetMatchAllBeforeDeciding(&boolContinueLoop); + result = boolContinueLoop; + + // If the operator is Contains, then we can cheat and avoid having to parse + // addresses. This does open up potential spurious matches for punctuation + // (e.g., ; or <), but the likelihood of users intending to search for these + // and also being able to match them is rather low. This optimization is not + // applicable to any other search type. + if (m_operator == nsMsgSearchOp::Contains) + return MatchRfc2047String(string, charset, false, pResult); + + nsTArray<nsString> names, addresses; + ExtractAllAddresses(EncodedHeader(string, charset), names, addresses); + uint32_t count = names.Length(); + + nsresult rv = NS_OK; + for (uint32_t i = 0; i < count && result == boolContinueLoop; i++) { + if (m_operator == nsMsgSearchOp::IsInAB || + m_operator == nsMsgSearchOp::IsntInAB) { + rv = MatchInAddressBook(addresses[i], &result); + } else { + rv = MatchString(names[i], &result); + if (boolContinueLoop == result) rv = MatchString(addresses[i], &result); + } + } + *pResult = result; + return rv; +} + +nsresult nsMsgSearchTerm::GetLocalTimes(PRTime a, PRTime b, + PRExplodedTime& aExploded, + PRExplodedTime& bExploded) { + PR_ExplodeTime(a, PR_LocalTimeParameters, &aExploded); + PR_ExplodeTime(b, PR_LocalTimeParameters, &bExploded); + return NS_OK; +} + +nsresult nsMsgSearchTerm::MatchDate(PRTime dateToMatch, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv = NS_OK; + bool result = false; + + PRExplodedTime tmToMatch, tmThis; + if (NS_SUCCEEDED( + GetLocalTimes(dateToMatch, m_value.u.date, tmToMatch, tmThis))) { + switch (m_operator) { + case nsMsgSearchOp::IsBefore: + if (tmToMatch.tm_year < tmThis.tm_year || + (tmToMatch.tm_year == tmThis.tm_year && + tmToMatch.tm_yday < tmThis.tm_yday)) + result = true; + break; + case nsMsgSearchOp::IsAfter: + if (tmToMatch.tm_year > tmThis.tm_year || + (tmToMatch.tm_year == tmThis.tm_year && + tmToMatch.tm_yday > tmThis.tm_yday)) + result = true; + break; + case nsMsgSearchOp::Is: + if (tmThis.tm_year == tmToMatch.tm_year && + tmThis.tm_month == tmToMatch.tm_month && + tmThis.tm_mday == tmToMatch.tm_mday) + result = true; + break; + case nsMsgSearchOp::Isnt: + if (tmThis.tm_year != tmToMatch.tm_year || + tmThis.tm_month != tmToMatch.tm_month || + tmThis.tm_mday != tmToMatch.tm_mday) + result = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for dates"); + } + } + *pResult = result; + return rv; +} + +nsresult nsMsgSearchTerm::MatchAge(PRTime msgDate, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + bool result = false; + nsresult rv = NS_OK; + + PRTime now = PR_Now(); + PRTime cutOffDay = now - m_value.u.age * PR_USEC_PER_DAY; + + bool cutOffDayInTheFuture = m_value.u.age < 0; + + // So now cutOffDay is the PRTime cut-off point. + // Any msg with a time less than that will be past the age. + + switch (m_operator) { + case nsMsgSearchOp::IsGreaterThan: // is older than, or more in the future + if ((!cutOffDayInTheFuture && msgDate < cutOffDay) || + (cutOffDayInTheFuture && msgDate > cutOffDay)) + result = true; + break; + case nsMsgSearchOp::IsLessThan: // is younger than, or less in the future + if ((!cutOffDayInTheFuture && msgDate > cutOffDay) || + (cutOffDayInTheFuture && msgDate < cutOffDay)) + result = true; + break; + case nsMsgSearchOp::Is: + PRExplodedTime msgDateExploded; + PRExplodedTime cutOffDayExploded; + if (NS_SUCCEEDED(GetLocalTimes(msgDate, cutOffDay, msgDateExploded, + cutOffDayExploded))) { + if ((msgDateExploded.tm_mday == cutOffDayExploded.tm_mday) && + (msgDateExploded.tm_month == cutOffDayExploded.tm_month) && + (msgDateExploded.tm_year == cutOffDayExploded.tm_year)) + result = true; + } + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for msg age"); + } + *pResult = result; + return rv; +} + +nsresult nsMsgSearchTerm::MatchSize(uint32_t sizeToMatch, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv = NS_OK; + bool result = false; + // We reduce the sizeToMatch rather than supplied size + // as then we can do an exact match on the displayed value + // which will be less confusing to the user. + uint32_t sizeToMatchKB = sizeToMatch; + + if (sizeToMatchKB < 1024) sizeToMatchKB = 1024; + + sizeToMatchKB /= 1024; + + switch (m_operator) { + case nsMsgSearchOp::IsGreaterThan: + if (sizeToMatchKB > m_value.u.size) result = true; + break; + case nsMsgSearchOp::IsLessThan: + if (sizeToMatchKB < m_value.u.size) result = true; + break; + case nsMsgSearchOp::Is: + if (sizeToMatchKB == m_value.u.size) result = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for size to match"); + } + *pResult = result; + return rv; +} + +nsresult nsMsgSearchTerm::MatchJunkStatus(const char* aJunkScore, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + if (m_operator == nsMsgSearchOp::IsEmpty) { + *pResult = !(aJunkScore && *aJunkScore); + return NS_OK; + } + if (m_operator == nsMsgSearchOp::IsntEmpty) { + *pResult = (aJunkScore && *aJunkScore); + return NS_OK; + } + + nsMsgJunkStatus junkStatus; + if (aJunkScore && *aJunkScore) { + junkStatus = (atoi(aJunkScore) == nsIJunkMailPlugin::IS_SPAM_SCORE) + ? nsIJunkMailPlugin::JUNK + : nsIJunkMailPlugin::GOOD; + } else { + // the in UI, we only show "junk" or "not junk" + // unknown, or nsIJunkMailPlugin::UNCLASSIFIED is shown as not junk + // so for the search to work as expected, treat unknown as not junk + junkStatus = nsIJunkMailPlugin::GOOD; + } + + nsresult rv = NS_OK; + bool matches = (junkStatus == m_value.u.junkStatus); + + switch (m_operator) { + case nsMsgSearchOp::Is: + break; + case nsMsgSearchOp::Isnt: + matches = !matches; + break; + default: + rv = NS_ERROR_FAILURE; + matches = false; + NS_ERROR("invalid compare op for junk status"); + } + + *pResult = matches; + return rv; +} + +nsresult nsMsgSearchTerm::MatchJunkScoreOrigin(const char* aJunkScoreOrigin, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + bool matches = false; + nsresult rv = NS_OK; + + switch (m_operator) { + case nsMsgSearchOp::Is: + matches = aJunkScoreOrigin && m_value.utf8String.Equals(aJunkScoreOrigin); + break; + case nsMsgSearchOp::Isnt: + matches = + !aJunkScoreOrigin || !m_value.utf8String.Equals(aJunkScoreOrigin); + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for junk score origin"); + } + + *pResult = matches; + return rv; +} + +nsresult nsMsgSearchTerm::MatchJunkPercent(uint32_t aJunkPercent, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv = NS_OK; + bool result = false; + switch (m_operator) { + case nsMsgSearchOp::IsGreaterThan: + if (aJunkPercent > m_value.u.junkPercent) result = true; + break; + case nsMsgSearchOp::IsLessThan: + if (aJunkPercent < m_value.u.junkPercent) result = true; + break; + case nsMsgSearchOp::Is: + if (aJunkPercent == m_value.u.junkPercent) result = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for junk percent"); + } + *pResult = result; + return rv; +} + +// MatchStatus () is not only used for nsMsgMessageFlags but also for +// nsMsgFolderFlags (both being 'unsigned long') +nsresult nsMsgSearchTerm::MatchStatus(uint32_t statusToMatch, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv = NS_OK; + bool matches = (statusToMatch & m_value.u.msgStatus); + + // nsMsgSearchOp::Is and nsMsgSearchOp::Isnt are intentionally used as + // Contains and DoesntContain respectively, for legacy reasons. + switch (m_operator) { + case nsMsgSearchOp::Is: + break; + case nsMsgSearchOp::Isnt: + matches = !matches; + break; + default: + rv = NS_ERROR_FAILURE; + matches = false; + NS_ERROR("invalid compare op for msg status"); + } + + *pResult = matches; + return rv; +} + +/* + * MatchKeyword Logic table (*pResult: + is true, - is false) + * + * # Valid Tokens IsEmpty IsntEmpty Contains DoesntContain Is Isnt + * 0 + - - + - + + * Term found? N Y N Y N Y N Y + * 1 - + - + + - - + + - + * >1 - + - + + - - - + + + */ +// look up nsMsgSearchTerm::m_value in space-delimited keywordList +nsresult nsMsgSearchTerm::MatchKeyword(const nsACString& keywordList, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + bool matches = false; + + // special-case empty for performance reasons + if (keywordList.IsEmpty()) { + *pResult = m_operator != nsMsgSearchOp::Contains && + m_operator != nsMsgSearchOp::Is && + m_operator != nsMsgSearchOp::IsntEmpty; + return NS_OK; + } + + // check if we can skip expensive valid keywordList test + if (m_operator == nsMsgSearchOp::DoesntContain || + m_operator == nsMsgSearchOp::Contains) { + nsCString keywordString(keywordList); + const uint32_t kKeywordLen = m_value.utf8String.Length(); + const char* matchStart = + PL_strstr(keywordString.get(), m_value.utf8String.get()); + while (matchStart) { + // For a real match, matchStart must be the start of the keywordList or + // preceded by a space and matchEnd must point to a \0 or space. + const char* matchEnd = matchStart + kKeywordLen; + if ((matchStart == keywordString.get() || matchStart[-1] == ' ') && + (!*matchEnd || *matchEnd == ' ')) { + // found the keyword + *pResult = m_operator == nsMsgSearchOp::Contains; + return NS_OK; + } + // no match yet, so search on + matchStart = PL_strstr(matchEnd, m_value.utf8String.get()); + } + // keyword not found + *pResult = m_operator == nsMsgSearchOp::DoesntContain; + return NS_OK; + } + + // Only accept valid keys in tokens. + nsresult rv = NS_OK; + nsTArray<nsCString> keywordArray; + ParseString(keywordList, ' ', keywordArray); + nsCOMPtr<nsIMsgTagService> tagService( + do_GetService("@mozilla.org/messenger/tagservice;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Loop through tokens in keywords + uint32_t count = keywordArray.Length(); + for (uint32_t i = 0; i < count; i++) { + // is this token a valid tag? Otherwise ignore it + bool isValid; + rv = tagService->IsValidKey(keywordArray[i], &isValid); + NS_ENSURE_SUCCESS(rv, rv); + + if (isValid) { + // IsEmpty fails on any valid token + if (m_operator == nsMsgSearchOp::IsEmpty) { + *pResult = false; + return rv; + } + + // IsntEmpty succeeds on any valid token + if (m_operator == nsMsgSearchOp::IsntEmpty) { + *pResult = true; + return rv; + } + + // Does this valid tag key match our search term? + matches = keywordArray[i].Equals(m_value.utf8String); + + // Is or Isn't partly determined on a single unmatched token + if (!matches) { + if (m_operator == nsMsgSearchOp::Is) { + *pResult = false; + return rv; + } + if (m_operator == nsMsgSearchOp::Isnt) { + *pResult = true; + return rv; + } + } + } + } + + if (m_operator == nsMsgSearchOp::Is) { + *pResult = matches; + return NS_OK; + } + + if (m_operator == nsMsgSearchOp::Isnt) { + *pResult = !matches; + return NS_OK; + } + + if (m_operator == nsMsgSearchOp::IsEmpty) { + *pResult = true; + return NS_OK; + } + + if (m_operator == nsMsgSearchOp::IsntEmpty) { + *pResult = false; + return NS_OK; + } + + // no valid match operator found + *pResult = false; + NS_ERROR("invalid compare op for msg status"); + return NS_ERROR_FAILURE; +} + +nsresult nsMsgSearchTerm::MatchPriority(nsMsgPriorityValue priorityToMatch, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv = NS_OK; + bool result = false; + + // Use this ugly little hack to get around the fact that enums don't have + // integer compare operators + int p1 = (priorityToMatch == nsMsgPriority::none) ? (int)nsMsgPriority::normal + : (int)priorityToMatch; + int p2 = (int)m_value.u.priority; + + switch (m_operator) { + case nsMsgSearchOp::IsHigherThan: + if (p1 > p2) result = true; + break; + case nsMsgSearchOp::IsLowerThan: + if (p1 < p2) result = true; + break; + case nsMsgSearchOp::Is: + if (p1 == p2) result = true; + break; + case nsMsgSearchOp::Isnt: + if (p1 != p2) result = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for priority"); + } + *pResult = result; + return rv; +} + +// match a custom search term +NS_IMETHODIMP nsMsgSearchTerm::MatchCustom(nsIMsgDBHdr* aHdr, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv; + nsCOMPtr<nsIMsgFilterService> filterService = + do_GetService("@mozilla.org/messenger/services/filters;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgSearchCustomTerm> customTerm; + rv = filterService->GetCustomTerm(m_customId, getter_AddRefs(customTerm)); + NS_ENSURE_SUCCESS(rv, rv); + + if (customTerm) + return customTerm->Match(aHdr, m_value.utf8String, m_operator, pResult); + *pResult = false; // default to no match if term is missing + return NS_ERROR_FAILURE; // missing custom term +} + +// set the id of a custom search term +NS_IMETHODIMP nsMsgSearchTerm::SetCustomId(const nsACString& aId) { + m_customId = aId; + return NS_OK; +} + +// get the id of a custom search term +NS_IMETHODIMP nsMsgSearchTerm::GetCustomId(nsACString& aResult) { + aResult = m_customId; + return NS_OK; +} + +NS_IMPL_GETSET(nsMsgSearchTerm, Attrib, nsMsgSearchAttribValue, m_attribute) +NS_IMPL_GETSET(nsMsgSearchTerm, Op, nsMsgSearchOpValue, m_operator) +NS_IMPL_GETSET(nsMsgSearchTerm, MatchAll, bool, m_matchAll) + +NS_IMETHODIMP +nsMsgSearchTerm::GetValue(nsIMsgSearchValue** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_ADDREF(*aResult = new nsMsgSearchValueImpl(&m_value)); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::SetValue(nsIMsgSearchValue* aValue) { + nsMsgResultElement::AssignValues(aValue, &m_value); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::GetBooleanAnd(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = (m_booleanOp == nsMsgSearchBooleanOp::BooleanAND); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::SetBooleanAnd(bool aValue) { + if (aValue) + m_booleanOp = nsMsgSearchBooleanOperator(nsMsgSearchBooleanOp::BooleanAND); + else + m_booleanOp = nsMsgSearchBooleanOperator(nsMsgSearchBooleanOp::BooleanOR); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::GetArbitraryHeader(nsACString& aResult) { + aResult = m_arbitraryHeader; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::SetArbitraryHeader(const nsACString& aValue) { + m_arbitraryHeader = aValue; + ToLowerCaseExceptSpecials(m_arbitraryHeader); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::GetHdrProperty(nsACString& aResult) { + aResult = m_hdrProperty; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::SetHdrProperty(const nsACString& aValue) { + m_hdrProperty = aValue; + ToLowerCaseExceptSpecials(m_hdrProperty); + return NS_OK; +} + +NS_IMPL_GETSET(nsMsgSearchTerm, BeginsGrouping, bool, mBeginsGrouping) +NS_IMPL_GETSET(nsMsgSearchTerm, EndsGrouping, bool, mEndsGrouping) + +// +// Certain possible standard values of a message database row also sometimes +// appear as header values. To prevent a naming collision, we use all +// lower case for the standard headers, and first capital when those +// same strings are requested as arbitrary headers. This routine is used +// when setting arbitrary headers. +// +void nsMsgSearchTerm::ToLowerCaseExceptSpecials(nsACString& aValue) { + if (aValue.LowerCaseEqualsLiteral("sender")) + aValue.AssignLiteral("Sender"); + else if (aValue.LowerCaseEqualsLiteral("date")) + aValue.AssignLiteral("Date"); + else if (aValue.LowerCaseEqualsLiteral("status")) + aValue.AssignLiteral("Status"); + else + ToLowerCase(aValue); +} + +//----------------------------------------------------------------------------- +// nsMsgSearchScopeTerm implementation +//----------------------------------------------------------------------------- +nsMsgSearchScopeTerm::nsMsgSearchScopeTerm(nsIMsgSearchSession* session, + nsMsgSearchScopeValue attribute, + nsIMsgFolder* folder) { + m_attribute = attribute; + m_folder = folder; + m_searchServer = true; + m_searchSession = do_GetWeakReference(session); +} + +nsMsgSearchScopeTerm::nsMsgSearchScopeTerm() { m_searchServer = true; } + +nsMsgSearchScopeTerm::~nsMsgSearchScopeTerm() { + if (m_inputStream) m_inputStream->Close(); + m_inputStream = nullptr; +} + +NS_IMPL_ISUPPORTS(nsMsgSearchScopeTerm, nsIMsgSearchScopeTerm) + +NS_IMETHODIMP +nsMsgSearchScopeTerm::GetFolder(nsIMsgFolder** aResult) { + NS_IF_ADDREF(*aResult = m_folder); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchScopeTerm::GetSearchSession(nsIMsgSearchSession** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + nsCOMPtr<nsIMsgSearchSession> searchSession = + do_QueryReferent(m_searchSession); + searchSession.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchScopeTerm::GetInputStream(nsIMsgDBHdr* aMsgHdr, + nsIInputStream** aInputStream) { + NS_ENSURE_ARG_POINTER(aInputStream); + NS_ENSURE_ARG_POINTER(aMsgHdr); + NS_ENSURE_TRUE(m_folder, NS_ERROR_NULL_POINTER); + nsresult rv = + m_folder->GetMsgInputStream(aMsgHdr, getter_AddRefs(m_inputStream)); + NS_ENSURE_SUCCESS(rv, rv); + NS_IF_ADDREF(*aInputStream = m_inputStream); + return rv; +} + +NS_IMETHODIMP nsMsgSearchScopeTerm::CloseInputStream() { + if (m_inputStream) { + m_inputStream->Close(); + m_inputStream = nullptr; + } + return NS_OK; +} + +nsresult nsMsgSearchScopeTerm::TimeSlice(bool* aDone) { + return m_adapter->Search(aDone); +} + +nsresult nsMsgSearchScopeTerm::InitializeAdapter( + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList) { + if (m_adapter) return NS_OK; + + nsresult rv = NS_OK; + + switch (m_attribute) { + case nsMsgSearchScope::onlineMail: + m_adapter = new nsMsgSearchOnlineMail(this, termList); + break; + case nsMsgSearchScope::offlineMail: + case nsMsgSearchScope::onlineManual: + m_adapter = new nsMsgSearchOfflineMail(this, termList); + break; + case nsMsgSearchScope::newsEx: + NS_ASSERTION(false, "not supporting newsEx yet"); + break; + case nsMsgSearchScope::news: + m_adapter = new nsMsgSearchNews(this, termList); + break; + case nsMsgSearchScope::allSearchableGroups: + NS_ASSERTION(false, "not supporting allSearchableGroups yet"); + break; + case nsMsgSearchScope::LDAP: + NS_ASSERTION(false, "not supporting LDAP yet"); + break; + case nsMsgSearchScope::localNews: + case nsMsgSearchScope::localNewsJunk: + case nsMsgSearchScope::localNewsBody: + case nsMsgSearchScope::localNewsJunkBody: + m_adapter = new nsMsgSearchOfflineNews(this, termList); + break; + default: + NS_ASSERTION(false, "invalid scope"); + rv = NS_ERROR_FAILURE; + } + + if (m_adapter) rv = m_adapter->ValidateTerms(); + + return rv; +} + +char* nsMsgSearchScopeTerm::GetStatusBarName() { return nullptr; } + +//----------------------------------------------------------------------------- +// nsMsgResultElement implementation +//----------------------------------------------------------------------------- + +nsMsgResultElement::nsMsgResultElement(nsIMsgSearchAdapter* adapter) { + m_adapter = adapter; +} + +nsMsgResultElement::~nsMsgResultElement() {} + +nsresult nsMsgResultElement::AddValue(nsIMsgSearchValue* value) { + m_valueList.AppendElement(value); + return NS_OK; +} + +nsresult nsMsgResultElement::AddValue(nsMsgSearchValue* value) { + nsMsgSearchValueImpl* valueImpl = new nsMsgSearchValueImpl(value); + delete value; // we keep the nsIMsgSearchValue, not + // the nsMsgSearchValue + return AddValue(valueImpl); +} + +nsresult nsMsgResultElement::AssignValues(nsIMsgSearchValue* src, + nsMsgSearchValue* dst) { + NS_ENSURE_ARG_POINTER(src); + NS_ENSURE_ARG_POINTER(dst); + // Yes, this could be an operator overload, but nsMsgSearchValue is totally + // public, so I'd have to define a derived class with nothing by operator=, + // and that seems like a bit much + nsresult rv = NS_OK; + src->GetAttrib(&dst->attribute); + switch (dst->attribute) { + case nsMsgSearchAttrib::Priority: + rv = src->GetPriority(&dst->u.priority); + break; + case nsMsgSearchAttrib::Date: + rv = src->GetDate(&dst->u.date); + break; + case nsMsgSearchAttrib::HasAttachmentStatus: + case nsMsgSearchAttrib::MsgStatus: + case nsMsgSearchAttrib::FolderFlag: + case nsMsgSearchAttrib::Uint32HdrProperty: + rv = src->GetStatus(&dst->u.msgStatus); + break; + case nsMsgSearchAttrib::MessageKey: + rv = src->GetMsgKey(&dst->u.key); + break; + case nsMsgSearchAttrib::AgeInDays: + rv = src->GetAge(&dst->u.age); + break; + case nsMsgSearchAttrib::JunkStatus: + rv = src->GetJunkStatus(&dst->u.junkStatus); + break; + case nsMsgSearchAttrib::JunkPercent: + rv = src->GetJunkPercent(&dst->u.junkPercent); + break; + case nsMsgSearchAttrib::Size: + rv = src->GetSize(&dst->u.size); + break; + default: + if (dst->attribute < nsMsgSearchAttrib::kNumMsgSearchAttributes) { + NS_ASSERTION(IS_STRING_ATTRIBUTE(dst->attribute), + "assigning non-string result"); + nsString unicodeString; + rv = src->GetStr(unicodeString); + CopyUTF16toUTF8(unicodeString, dst->utf8String); + dst->utf16String = unicodeString; + } else + rv = NS_ERROR_INVALID_ARG; + } + return rv; +} + +nsresult nsMsgResultElement::GetValue(nsMsgSearchAttribValue attrib, + nsMsgSearchValue** outValue) const { + nsresult rv = NS_OK; + *outValue = NULL; + + for (uint32_t i = 0; i < m_valueList.Length() && NS_FAILED(rv); i++) { + nsMsgSearchAttribValue valueAttribute; + m_valueList[i]->GetAttrib(&valueAttribute); + if (attrib == valueAttribute) { + *outValue = new nsMsgSearchValue; + if (*outValue) { + rv = AssignValues(m_valueList[i], *outValue); + // Now this is really strange! What is this assignment doing here? + rv = NS_OK; + } else + rv = NS_ERROR_OUT_OF_MEMORY; + } + } + return rv; +} + +nsresult nsMsgResultElement::GetPrettyName(nsMsgSearchValue** value) { + return GetValue(nsMsgSearchAttrib::Location, value); +} + +nsresult nsMsgResultElement::Open(void* window) { + return NS_ERROR_NULL_POINTER; +} diff --git a/comm/mailnews/search/src/nsMsgSearchValue.cpp b/comm/mailnews/search/src/nsMsgSearchValue.cpp new file mode 100644 index 0000000000..55f88b5fd2 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgSearchValue.cpp @@ -0,0 +1,96 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "MailNewsTypes.h" +#include "nsMsgSearchValue.h" +#include "nsIMsgFolder.h" +#include "nsMsgUtils.h" +#include "nsString.h" + +nsMsgSearchValueImpl::nsMsgSearchValueImpl(nsMsgSearchValue* aInitialValue) { + mValue = *aInitialValue; +} + +nsMsgSearchValueImpl::~nsMsgSearchValueImpl() {} + +NS_IMPL_ISUPPORTS(nsMsgSearchValueImpl, nsIMsgSearchValue) + +NS_IMPL_GETSET(nsMsgSearchValueImpl, Priority, nsMsgPriorityValue, + mValue.u.priority) +NS_IMPL_GETSET(nsMsgSearchValueImpl, Status, uint32_t, mValue.u.msgStatus) +NS_IMPL_GETSET(nsMsgSearchValueImpl, Size, uint32_t, mValue.u.size) +NS_IMPL_GETSET(nsMsgSearchValueImpl, MsgKey, nsMsgKey, mValue.u.key) +NS_IMPL_GETSET(nsMsgSearchValueImpl, Age, int32_t, mValue.u.age) +NS_IMPL_GETSET(nsMsgSearchValueImpl, Date, PRTime, mValue.u.date) +NS_IMPL_GETSET(nsMsgSearchValueImpl, Attrib, nsMsgSearchAttribValue, + mValue.attribute) +NS_IMPL_GETSET(nsMsgSearchValueImpl, JunkStatus, uint32_t, mValue.u.junkStatus) +NS_IMPL_GETSET(nsMsgSearchValueImpl, JunkPercent, uint32_t, + mValue.u.junkPercent) + +NS_IMETHODIMP +nsMsgSearchValueImpl::GetFolder(nsIMsgFolder** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_ENSURE_TRUE(mValue.attribute == nsMsgSearchAttrib::FolderInfo, + NS_ERROR_ILLEGAL_VALUE); + NS_IF_ADDREF(*aResult = mValue.u.folder); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchValueImpl::SetFolder(nsIMsgFolder* aValue) { + NS_ENSURE_TRUE(mValue.attribute == nsMsgSearchAttrib::FolderInfo, + NS_ERROR_ILLEGAL_VALUE); + mValue.u.folder = aValue; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchValueImpl::GetStr(nsAString& aResult) { + NS_ENSURE_TRUE(IS_STRING_ATTRIBUTE(mValue.attribute), NS_ERROR_ILLEGAL_VALUE); + aResult = mValue.utf16String; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchValueImpl::SetStr(const nsAString& aValue) { + NS_ENSURE_TRUE(IS_STRING_ATTRIBUTE(mValue.attribute), NS_ERROR_ILLEGAL_VALUE); + CopyUTF16toUTF8(aValue, mValue.utf8String); + mValue.utf16String = aValue; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchValueImpl::ToString(nsAString& aResult) { + aResult.AssignLiteral("[nsIMsgSearchValue: "); + if (IS_STRING_ATTRIBUTE(mValue.attribute)) { + aResult.Append(mValue.utf16String); + return NS_OK; + } + + switch (mValue.attribute) { + case nsMsgSearchAttrib::Priority: + case nsMsgSearchAttrib::Date: + case nsMsgSearchAttrib::MsgStatus: + case nsMsgSearchAttrib::MessageKey: + case nsMsgSearchAttrib::Size: + case nsMsgSearchAttrib::AgeInDays: + case nsMsgSearchAttrib::FolderInfo: + case nsMsgSearchAttrib::JunkStatus: + case nsMsgSearchAttrib::JunkPercent: { + nsAutoString tempInt; + tempInt.AppendInt(mValue.attribute); + + aResult.AppendLiteral("type="); + aResult.Append(tempInt); + } break; + default: + NS_ERROR("Unknown search value type"); + } + + aResult.Append(']'); + + return NS_OK; +} diff --git a/comm/mailnews/search/src/nsMsgSearchValue.h b/comm/mailnews/search/src/nsMsgSearchValue.h new file mode 100644 index 0000000000..225ee64760 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgSearchValue.h @@ -0,0 +1,25 @@ +/* -*- 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/. */ + +#ifndef __nsMsgSearchValue_h +#define __nsMsgSearchValue_h + +#include "nsIMsgSearchValue.h" +#include "nsMsgSearchCore.h" + +class nsMsgSearchValueImpl : public nsIMsgSearchValue { + public: + explicit nsMsgSearchValueImpl(nsMsgSearchValue* aInitialValue); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGSEARCHVALUE + + private: + virtual ~nsMsgSearchValueImpl(); + + nsMsgSearchValue mValue; +}; + +#endif diff --git a/comm/mailnews/search/test/moz.build b/comm/mailnews/search/test/moz.build new file mode 100644 index 0000000000..6b37fdbe09 --- /dev/null +++ b/comm/mailnews/search/test/moz.build @@ -0,0 +1,6 @@ +# vim: set filetype=python: +# 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/. + +XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"] diff --git a/comm/mailnews/search/test/unit/head_mailbase.js b/comm/mailnews/search/test/unit/head_mailbase.js new file mode 100644 index 0000000000..9f37623291 --- /dev/null +++ b/comm/mailnews/search/test/unit/head_mailbase.js @@ -0,0 +1,23 @@ +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { mailTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/MailTestUtils.jsm" +); +var { localAccountUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/LocalAccountUtils.jsm" +); + +var CC = Components.Constructor; + +// Ensure the profile directory is set up +do_get_profile(); + +var gDEPTH = "../../../../"; + +registerCleanupFunction(function () { + load(gDEPTH + "mailnews/resources/mailShutdown.js"); +}); diff --git a/comm/mailnews/search/test/unit/test_base64_decoding.js b/comm/mailnews/search/test/unit/test_base64_decoding.js new file mode 100644 index 0000000000..e0f7ddd94d --- /dev/null +++ b/comm/mailnews/search/test/unit/test_base64_decoding.js @@ -0,0 +1,94 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This tests that we do not crash when loading the email bodySearchCrash, +// which was fixed in bug 465805 + +/* import-globals-from ../../../test/resources/searchTestUtils.js */ +load("../../../resources/searchTestUtils.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var Contains = Ci.nsMsgSearchOp.Contains; +var Body = Ci.nsMsgSearchAttrib.Body; + +var Files = [ + "../../../data/bugmail1", + "../../../data/bodySearchCrash", // Test for bug 465805. + "../../../data/base64-with-whitespace.eml", // Test for bug 1487421. +]; + +var Tests = [ + { + // this number appears in bugmail1 + value: "432710", + attrib: Body, + op: Contains, + count: 1, + }, + { + // this appears in base64-with-whitespace.eml + value: "abcdefghijklmnopqrstuvwxyz", + attrib: Body, + op: Contains, + count: 1, + }, +]; + +function run_test() { + // Setup local mail accounts. + localAccountUtils.loadLocalMailAccount(); + + // Get a message into the local filestore. function testBodySearch() continues the testing after the copy. + do_test_pending(); + copyListener.OnStopCopy(null); + return true; +} + +var copyListener = { + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) {}, + SetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + let fileName = Files.shift(); + if (fileName) { + let file = do_get_file(fileName); + MailServices.copy.copyFileMessage( + file, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + copyListener, + null + ); + } else { + testBodySearch(); + } + }, +}; + +// Runs at completion of copy + +// process each test from queue, calls itself upon completion of each search +function testBodySearch() { + print("Test Body Search"); + var test = Tests.shift(); + if (test) { + new TestSearch( + localAccountUtils.inboxFolder, + test.value, + test.attrib, + test.op, + test.count, + testBodySearch + ); + } else { + do_test_finished(); + } +} diff --git a/comm/mailnews/search/test/unit/test_bug366491.js b/comm/mailnews/search/test/unit/test_bug366491.js new file mode 100644 index 0000000000..774e8932a3 --- /dev/null +++ b/comm/mailnews/search/test/unit/test_bug366491.js @@ -0,0 +1,110 @@ +/* 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/. */ + +// tests return of junk percent from bayesian filter + +// main setup + +// only needed during debug +// do_import_script("mailnews/extensions/bayesian-spam-filter/test/resources/trainingfile.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +// local constants +var kUnclassified = MailServices.junk.UNCLASSIFIED; +var kJunk = MailServices.junk.JUNK; +var kGood = MailServices.junk.GOOD; + +/* + * This test is not intended to check the spam calculations, + * but only that the junk percent is transmitted (particularly + * for intermediate values). The test + * junkPercent values below were calculated by the plugin, + * not indepedently verified. + */ + +var tests = [ + { + fileName: "ham2.eml", + junkPercent: 8, + }, + { + fileName: "spam2.eml", + junkPercent: 81, + }, +]; + +var emails = [ + { + fileName: "ham1.eml", + classification: kGood, + }, + { + fileName: "spam1.eml", + classification: kJunk, + }, +]; + +// main test +function run_test() { + localAccountUtils.loadLocalMailAccount(); + do_test_pending(); + doTestingListener.onMessageClassified(null, null, null); + return true; +} + +var haveClassification = false; +var doTestingListener = { + onMessageClassified(aMsgURI, aClassification, aJunkPercent) { + // Do we have more training emails? If so, train + var email = emails.shift(); + if (email) { + MailServices.junk.setMessageClassification( + getSpec(email.fileName), + kUnclassified, + email.classification, + null, + doTestingListener + ); + return; + } + + if (!aMsgURI) { + // Ignore end of batch. + return; + } + + // Have we completed a classification? If so, test + if (haveClassification) { + let test = tests.shift(); + Assert.equal(getSpec(test.fileName), aMsgURI); + Assert.equal(test.junkPercent, aJunkPercent); + } + + // Do we have more classifications to do? Then classify the first one. + if (tests.length) { + haveClassification = true; + MailServices.junk.classifyMessage( + getSpec(tests[0].fileName), + null, + doTestingListener + ); + } else { + do_test_finished(); + } + }, +}; + +// helper functions + +function getSpec(aFileName) { + var file = do_get_file( + "../../../extensions/bayesian-spam-filter/test/unit/resources/" + aFileName + ); + var uri = Services.io.newFileURI(file).QueryInterface(Ci.nsIURL); + uri = uri.mutate().setQuery("type=application/x-message-display").finalize(); + return uri.spec; +} diff --git a/comm/mailnews/search/test/unit/test_bug404489.js b/comm/mailnews/search/test/unit/test_bug404489.js new file mode 100644 index 0000000000..93ae2aac3c --- /dev/null +++ b/comm/mailnews/search/test/unit/test_bug404489.js @@ -0,0 +1,202 @@ +/* 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/. */ + +// Tests that custom headers like "Sender" work (bug 404489) + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var Contains = Ci.nsMsgSearchOp.Contains; +var gArrayHdrs = ["X-Bugzilla-Who", "Sender"]; +var gFirstHeader = Ci.nsMsgSearchAttrib.OtherHeader + 1; +var fileName = "../../../data/SenderHeader"; + +var Tests = [ + /* test header: + X-Bugzilla-Who: bugmail@example.org + + This just shows that normal custom headers work + */ + { + testValue: "bugmail", + attrib: gFirstHeader, + op: Contains, + count: 1, + }, + { + testValue: "ThisIsNotThere", + attrib: gFirstHeader, + op: Contains, + count: 0, + }, + /* test header: + Sender: iamthesender@example.com + + This is the main fix of bug 404489, that we can use Sender as a header + */ + { + testValue: "iamthesender", + attrib: gFirstHeader + 1, + op: Contains, + count: 1, + }, + /* test header: + From: bugzilla-daemon@mozilla.invalid + + Here we show that the "From" header does not fire tests for the + "Sender" arbitrary headers, but does fire the standard test + for nsMsgSenderAttrib.Sender + */ + { + testValue: "bugzilla", + attrib: gFirstHeader + 1, + op: Contains, + count: 0, + }, + { + testValue: "bugzilla", + attrib: Ci.nsMsgSearchAttrib.Sender, + op: Contains, + count: 1, + }, +]; + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + + // add the custom headers into the preferences file, ":" delimited + + var hdrs; + if (gArrayHdrs.length == 1) { + hdrs = gArrayHdrs; + } else { + hdrs = gArrayHdrs.join(": "); + } + Services.prefs.setCharPref("mailnews.customHeaders", hdrs); + + // Get a message into the local filestore. function continue_test() continues the testing after the copy. + do_test_pending(); + var file = do_get_file(fileName); + MailServices.copy.copyFileMessage( + file, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + copyListener, + null + ); + return true; +} + +var copyListener = { + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) {}, + SetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + continue_test(); + }, +}; + +// Runs at completion of each copy +// process each test from queue, calls itself upon completion of each search +function continue_test() { + var test = Tests.shift(); + if (test) { + new TestSearchx( + localAccountUtils.inboxFolder, + test.testValue, + test.attrib, + test.op, + test.count, + continue_test + ); + } else { + do_test_finished(); + } +} + +/* + * TestSearchx: Class to test number of search hits + * + * @param aFolder: the folder to search + * @param aValue: value used for the search + * The interpretation of aValue depends on aAttrib. It + * defaults to string, but for certain attributes other + * types are used. + * WARNING: not all attributes have been tested. + * + * @param aAttrib: attribute for the search (Ci.nsMsgSearchAttrib.Size, etc.) + * @param aOp: operation for the search (Ci.nsMsgSearchOp.Contains, etc.) + * @param aHitCount: expected number of search hits + * @param onDone: function to call on completion of search + * + */ + +function TestSearchx(aFolder, aValue, aAttrib, aOp, aHitCount, onDone) { + var searchListener = { + onSearchHit(dbHdr, folder) { + hitCount++; + }, + onSearchDone(status) { + print("Finished search does " + aHitCount + " equal " + hitCount + "?"); + searchSession = null; + Assert.equal(aHitCount, hitCount); + if (onDone) { + onDone(); + } + }, + onNewSearch() { + hitCount = 0; + }, + }; + + // define and initiate the search session + + var hitCount; + var searchSession = Cc[ + "@mozilla.org/messenger/searchSession;1" + ].createInstance(Ci.nsIMsgSearchSession); + searchSession.addScopeTerm(Ci.nsMsgSearchScope.offlineMail, aFolder); + var searchTerm = searchSession.createTerm(); + searchTerm.attrib = aAttrib; + + var value = searchTerm.value; + // This is tricky - value.attrib must be set before actual values + value.attrib = aAttrib; + if (aAttrib == Ci.nsMsgSearchAttrib.JunkPercent) { + value.junkPercent = aValue; + } else if (aAttrib == Ci.nsMsgSearchAttrib.Priority) { + value.priority = aValue; + } else if (aAttrib == Ci.nsMsgSearchAttrib.Date) { + value.date = aValue; + } else if (aAttrib == Ci.nsMsgSearchAttrib.MsgStatus) { + value.status = aValue; + } else if (aAttrib == Ci.nsMsgSearchAttrib.MessageKey) { + value.msgKey = aValue; + } else if (aAttrib == Ci.nsMsgSearchAttrib.Size) { + value.size = aValue; + } else if (aAttrib == Ci.nsMsgSearchAttrib.AgeInDays) { + value.age = aValue; + } else if (aAttrib == Ci.nsMsgSearchAttrib.JunkStatus) { + value.junkStatus = aValue; + } else if (aAttrib == Ci.nsMsgSearchAttrib.HasAttachmentStatus) { + value.status = Ci.nsMsgMessageFlags.Attachment; + } else { + value.str = aValue; + } + searchTerm.value = value; + if (aAttrib > Ci.nsMsgSearchAttrib.OtherHeader) { + searchTerm.arbitraryHeader = + gArrayHdrs[aAttrib - 1 - Ci.nsMsgSearchAttrib.OtherHeader]; + } + searchTerm.op = aOp; + searchTerm.booleanAnd = false; + searchSession.appendTerm(searchTerm); + searchSession.registerListener(searchListener); + searchSession.search(null); +} diff --git a/comm/mailnews/search/test/unit/test_copyThenMoveManual.js b/comm/mailnews/search/test/unit/test_copyThenMoveManual.js new file mode 100644 index 0000000000..ef34aeb3ca --- /dev/null +++ b/comm/mailnews/search/test/unit/test_copyThenMoveManual.js @@ -0,0 +1,116 @@ +/* + * This file tests copy followed by a move in a single filter. + * Tests fix from bug 448337. + * + * Original author: Kent James <kent@caspia.com> + */ + +/* import-globals-from ../../../test/resources/POP3pump.js */ +load("../../../resources/POP3pump.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +var gFiles = ["../../../data/bugmail1"]; +var gCopyFolder; +var gMoveFolder; +var gFilter; // the test filter +var gFilterList; +var gTestArray = [ + function createFilters() { + // setup manual copy then move mail filters on the inbox + gFilterList = localAccountUtils.incomingServer.getFilterList(null); + gFilter = gFilterList.createFilter("copyThenMoveAll"); + let searchTerm = gFilter.createTerm(); + searchTerm.matchAll = true; + gFilter.appendTerm(searchTerm); + let copyAction = gFilter.createAction(); + copyAction.type = Ci.nsMsgFilterAction.CopyToFolder; + copyAction.targetFolderUri = gCopyFolder.URI; + gFilter.appendAction(copyAction); + let moveAction = gFilter.createAction(); + moveAction.type = Ci.nsMsgFilterAction.MoveToFolder; + moveAction.targetFolderUri = gMoveFolder.URI; + gFilter.appendAction(moveAction); + gFilter.enabled = true; + gFilter.filterType = Ci.nsMsgFilterType.Manual; + gFilterList.insertFilterAt(0, gFilter); + }, + // just get a message into the local folder + async function getLocalMessages1() { + gPOP3Pump.files = gFiles; + await gPOP3Pump.run(); + }, + // test applying filters to a message header + async function applyFilters() { + let promiseFolderEvent = PromiseTestUtils.promiseFolderEvent( + localAccountUtils.inboxFolder, + "DeleteOrMoveMsgCompleted" + ); + MailServices.filters.applyFilters( + Ci.nsMsgFilterType.Manual, + [localAccountUtils.inboxFolder.firstNewMessage], + localAccountUtils.inboxFolder, + null + ); + await promiseFolderEvent; + }, + function verifyFolders1() { + // Copy and Move should each now have 1 message in them. + Assert.equal(folderCount(gCopyFolder), 1); + Assert.equal(folderCount(gMoveFolder), 1); + // the local inbox folder should now be empty, since the second + // operation was a move + Assert.equal(folderCount(localAccountUtils.inboxFolder), 0); + }, + // just get a message into the local folder + async function getLocalMessages2() { + gPOP3Pump.files = gFiles; + await gPOP3Pump.run(); + }, + // use the alternate call into the filter service + async function applyFiltersToFolders() { + let folders = [localAccountUtils.inboxFolder]; + let promiseFolderEvent = PromiseTestUtils.promiseFolderEvent( + localAccountUtils.inboxFolder, + "DeleteOrMoveMsgCompleted" + ); + MailServices.filters.applyFiltersToFolders(gFilterList, folders, null); + await promiseFolderEvent; + }, + function verifyFolders2() { + // Copy and Move should each now have 2 message in them. + Assert.equal(folderCount(gCopyFolder), 2); + Assert.equal(folderCount(gMoveFolder), 2); + // the local inbox folder should now be empty, since the second + // operation was a move + Assert.equal(folderCount(localAccountUtils.inboxFolder), 0); + }, + function endTest() { + // Cleanup, null out everything, close all cached connections and stop the + // server + dump(" Exiting mail tests\n"); + gPOP3Pump = null; + }, +]; + +function folderCount(folder) { + return [...folder.msgDatabase.enumerateMessages()].length; +} + +function run_test() { + if (!localAccountUtils.inboxFolder) { + localAccountUtils.loadLocalMailAccount(); + } + + gCopyFolder = localAccountUtils.rootFolder.createLocalSubfolder("CopyFolder"); + gMoveFolder = localAccountUtils.rootFolder.createLocalSubfolder("MoveFolder"); + + gTestArray.forEach(x => add_task(x)); + + run_next_test(); +} diff --git a/comm/mailnews/search/test/unit/test_junkWhitelisting.js b/comm/mailnews/search/test/unit/test_junkWhitelisting.js new file mode 100644 index 0000000000..9263de68a2 --- /dev/null +++ b/comm/mailnews/search/test/unit/test_junkWhitelisting.js @@ -0,0 +1,204 @@ +/* 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/. */ + +/* + * Testing of junk whitelisting + */ + +// add address book setup +/* import-globals-from ../../../test/resources/abSetup.js */ +load("../../../resources/abSetup.js"); + +// add fake POP3 server driver +/* import-globals-from ../../../test/resources/POP3pump.js */ +load("../../../resources/POP3pump.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +/* + * The address available in the test address book is "PrimaryEmail1@test.invalid" + * Test emails may also include the address "invalid@example.com" + * + * Map of test email contents: (P is "Prim...", I is "inva.." address) + * + * Index Bugmail# From + * 0 1 P + * 1 3 I + * + */ + +// indices into hdrs[] of email by domain +var kDomainTest = 0; +var kDomainExample = 1; + +var Files = ["../../../data/bugmail1", "../../../data/bugmail3"]; + +var hdrs = []; + +function run_test() { + loadABFile( + "../../../addrbook/test/unit/data/cardForEmail", + kPABData.fileName + ); + + do_test_pending(); + + // kick off copying + gPOP3Pump.files = Files; + gPOP3Pump.onDone = continueTest; + gPOP3Pump.run(); +} + +function continueTest() { + // get the message headers + for (let header of localAccountUtils.inboxFolder.messages) { + hdrs.push(header); + } + + // check with spam properties set on the local server + doChecks(localAccountUtils.incomingServer); + + // Free our globals + hdrs = null; + gPOP3Pump = null; + do_test_finished(); +} + +function doChecks(server) { + let spamSettings = server.spamSettings; + + // default is to use the whitelist + Assert.ok(spamSettings.useWhiteList); + + // check email with the address PrimaryEmail1@test.invalid + Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest])); + + // check email without the address + Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainExample])); + + // + // check changes in server-level settings. Although the spamSettings object + // has methods to set these, those methods are not persistent (which seems + // strange). You need to set the actual preference, and call initialize on + // spam settings, to get the settings to be saved persistently and stick, then + // be recalled into the program. So that's the way that I will test it. + // + + // disable whitelisting + server.setBoolValue("useWhiteList", false); + spamSettings.initialize(server); + + // check that the change was propagated to spamSettings + Assert.ok(!spamSettings.useWhiteList); + + // and affects whitelisting calculationss + Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainTest])); + + // re-enable whitelisting + server.setBoolValue("useWhiteList", true); + spamSettings.initialize(server); + Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest])); + + // Set an empty white list. + // To really empty this, I have to change the default value as well + Services.prefs.setCharPref("mail.server.default.whiteListAbURI", ""); + server.setCharValue("whiteListAbURI", ""); + spamSettings.initialize(server); + Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainTest])); + + // add a trusted domain. This is a global preference + Services.prefs.setCharPref("mail.trusteddomains", "example.com"); + spamSettings.initialize(server); + + // check email with the address invalid@example.com, a trusted domain + Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainExample])); + + // check email without the address + Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainTest])); + + // disable the trusted domain + Services.prefs.setCharPref("mail.trusteddomains", ""); + spamSettings.initialize(server); + Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainExample])); + + // add back the Personal Address Book + server.setCharValue("whiteListAbURI", kPABData.URI); + spamSettings.initialize(server); + Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest])); + + /* + * tests of whitelist suppression by identity + */ + + // setup + let account = MailServices.accounts.FindAccountForServer(server); + let identity = MailServices.accounts.createIdentity(); + // start with an email that does not match + identity.email = "iAmNotTheSender@test.invalid"; + account.addIdentity(identity); + + // setup account and identify for the deferred-from fake server + let fakeAccount = MailServices.accounts.createAccount(); + fakeAccount.incomingServer = gPOP3Pump.fakeServer; + let fakeIdentity = MailServices.accounts.createIdentity(); + // start with an email that does not match + fakeIdentity.email = "iAmNotTheSender@wrong.invalid"; + fakeAccount.addIdentity(fakeIdentity); + + // gPOP3Pump delivers messages to the local inbox regardless of other + // settings. But because we are testing here one of those other settings, + // let's just pretend that it works like the real POP3 stuff, and set + // the correct setting for deferring. + gPOP3Pump.fakeServer.setCharValue("deferred_to_account", "account1"); + + // suppress whitelisting for sender + server.setBoolValue("inhibitWhiteListingIdentityUser", true); + spamSettings.initialize(server); + // (email does not match yet though) + Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest])); + + // add a matching email (mixing case) + identity.email = "PrimaryEMAIL1@test.INVALID"; + spamSettings.initialize(server); + Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainTest])); + + // remove the matching email + identity.email = "iAmNotTheSender@test.invalid"; + spamSettings.initialize(server); + Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest])); + + // add the email to the deferred-from server + fakeIdentity.email = "PrimaryEMAIL1@test.INVALID"; + spamSettings.initialize(server); + Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainTest])); + + // stop suppressing identity users + server.setBoolValue("inhibitWhiteListingIdentityUser", false); + spamSettings.initialize(server); + Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest])); + + // remove the matching email from the fake identity + fakeIdentity.email = "iAmNotTheSender@wrong.invalid"; + + // add a fully non-matching domain to the identity + identity.email = "PrimaryEmail1@wrong.invalid"; + + // suppress whitelist by matching domain + server.setBoolValue("inhibitWhiteListingIdentityDomain", true); + spamSettings.initialize(server); + // but domain still does not match + Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest])); + + // add a matching email to the identity, in the domain (mixing case) + identity.email = "iAmNotTheSender@TEST.invalid"; + spamSettings.initialize(server); + Assert.ok(!spamSettings.checkWhiteList(hdrs[kDomainTest])); + + // stop suppressing whitelist by domain + server.setBoolValue("inhibitWhiteListingIdentityDomain", false); + spamSettings.initialize(server); + Assert.ok(spamSettings.checkWhiteList(hdrs[kDomainTest])); +} diff --git a/comm/mailnews/search/test/unit/test_quarantineFilterMove.js b/comm/mailnews/search/test/unit/test_quarantineFilterMove.js new file mode 100644 index 0000000000..853a3980f4 --- /dev/null +++ b/comm/mailnews/search/test/unit/test_quarantineFilterMove.js @@ -0,0 +1,181 @@ +/* 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/. */ + +/* + * tests message moves with filter and quarantine enabled per bug 582918. + * It then tests that subsequent moves of the filtered messages work. + * + * adapted from test_copyThenMoveManual.js + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +/* import-globals-from ../../../test/resources/POP3pump.js */ +load("../../../resources/POP3pump.js"); + +var gFiles = ["../../../data/bugmail1", "../../../data/bugmail10"]; + +var gMoveFolder, gMoveFolder2; +var gFilter; // the test filter +var gFilterList; +var gTestArray = [ + function createFilters() { + gFilterList = gPOP3Pump.fakeServer.getFilterList(null); + gFilter = gFilterList.createFilter("MoveAll"); + let searchTerm = gFilter.createTerm(); + searchTerm.matchAll = true; + gFilter.appendTerm(searchTerm); + let moveAction = gFilter.createAction(); + moveAction.type = Ci.nsMsgFilterAction.MoveToFolder; + moveAction.targetFolderUri = gMoveFolder.URI; + gFilter.appendAction(moveAction); + gFilter.enabled = true; + gFilter.filterType = Ci.nsMsgFilterType.InboxRule; + gFilterList.insertFilterAt(0, gFilter); + }, + // just get a message into the local folder + async function getLocalMessages1() { + gPOP3Pump.files = gFiles; + let promise1 = PromiseTestUtils.promiseFolderNotification( + gMoveFolder, + "msgsClassified" + ); + let promise2 = gPOP3Pump.run(); + await Promise.all([promise1, promise2]); + }, + async function verifyFolders1() { + Assert.equal(folderCount(gMoveFolder), 2); + // the local inbox folder should now be empty, since the second + // operation was a move + Assert.equal(folderCount(localAccountUtils.inboxFolder), 0); + + let msgs = [...gMoveFolder.msgDatabase.enumerateMessages()]; + let firstMsgHdr = msgs[0]; + let secondMsgHdr = msgs[1]; + // Check that the messages have content + let messageContent = await getContentFromMessage(firstMsgHdr); + Assert.ok( + messageContent.includes("Some User <bugmail@example.org> changed") + ); + messageContent = await getContentFromMessage(secondMsgHdr); + Assert.ok( + messageContent.includes( + "https://bugzilla.mozilla.org/show_bug.cgi?id=436880" + ) + ); + }, + async function copyMovedMessages() { + let msgs = [...gMoveFolder.msgDatabase.enumerateMessages()]; + let firstMsgHdr = msgs[0]; + let secondMsgHdr = msgs[1]; + let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener(); + MailServices.copy.copyMessages( + gMoveFolder, + [firstMsgHdr, secondMsgHdr], + gMoveFolder2, + false, + promiseCopyListener, + null, + false + ); + let promiseMoveMsg = PromiseTestUtils.promiseFolderEvent( + gMoveFolder, + "DeleteOrMoveMsgCompleted" + ); + await Promise.all([promiseCopyListener.promise, promiseMoveMsg]); + }, + async function verifyFolders2() { + Assert.equal(folderCount(gMoveFolder2), 2); + + let msgs = [...gMoveFolder2.msgDatabase.enumerateMessages()]; + let firstMsgHdr = msgs[0]; + let secondMsgHdr = msgs[1]; + // Check that the messages have content + let messageContent = await getContentFromMessage(firstMsgHdr); + Assert.ok( + messageContent.includes("Some User <bugmail@example.org> changed") + ); + messageContent = await getContentFromMessage(secondMsgHdr); + Assert.ok( + messageContent.includes( + "https://bugzilla.mozilla.org/show_bug.cgi?id=436880" + ) + ); + }, + function endTest() { + dump("Exiting mail tests\n"); + gPOP3Pump = null; + }, +]; + +function folderCount(folder) { + return [...folder.msgDatabase.enumerateMessages()].length; +} + +function run_test() { + /* may not work in Linux */ + // if ("@mozilla.org/gnome-gconf-service;1" in Cc) + // return; + /**/ + // quarantine messages + Services.prefs.setBoolPref("mailnews.downloadToTempFile", true); + if (!localAccountUtils.inboxFolder) { + localAccountUtils.loadLocalMailAccount(); + } + + gMoveFolder = localAccountUtils.rootFolder.createLocalSubfolder("MoveFolder"); + gMoveFolder2 = + localAccountUtils.rootFolder.createLocalSubfolder("MoveFolder2"); + + gTestArray.forEach(x => add_task(x)); + run_next_test(); +} + +/** + * Get the full message content. + * + * @param aMsgHdr - nsIMsgDBHdr object whose text body will be read. + * @returns {Promise<string>} full message contents. + */ +function getContentFromMessage(aMsgHdr) { + let msgFolder = aMsgHdr.folder; + let msgUri = msgFolder.getUriForMsg(aMsgHdr); + + return new Promise((resolve, reject) => { + let streamListener = { + QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]), + sis: Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ), + content: "", + onDataAvailable(request, inputStream, offset, count) { + this.sis.init(inputStream); + this.content += this.sis.read(count); + }, + onStartRequest(request) {}, + onStopRequest(request, statusCode) { + this.sis.close(); + if (Components.isSuccessCode(statusCode)) { + resolve(this.content); + } else { + reject(new Error(statusCode)); + } + }, + }; + MailServices.messageServiceFromURI(msgUri).streamMessage( + msgUri, + streamListener, + null, + null, + false, + "", + false + ); + }); +} diff --git a/comm/mailnews/search/test/unit/test_search.js b/comm/mailnews/search/test/unit/test_search.js new file mode 100644 index 0000000000..98e8874fd3 --- /dev/null +++ b/comm/mailnews/search/test/unit/test_search.js @@ -0,0 +1,623 @@ +/* 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/. */ + +/* + * Testing of general mail search features. + * + * This tests some search attributes not tested by other specific tests, + * e.g., test_searchTag.js or test_searchJunk.js + */ +/* import-globals-from ../../../test/resources/searchTestUtils.js */ +load("../../../resources/searchTestUtils.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var Isnt = Ci.nsMsgSearchOp.Isnt; +var Is = Ci.nsMsgSearchOp.Is; +var Contains = Ci.nsMsgSearchOp.Contains; +var DoesntContain = Ci.nsMsgSearchOp.DoesntContain; +var BeginsWith = Ci.nsMsgSearchOp.BeginsWith; +var EndsWith = Ci.nsMsgSearchOp.EndsWith; +var IsBefore = Ci.nsMsgSearchOp.IsBefore; // control entry not enabled +var IsAfter = Ci.nsMsgSearchOp.IsAfter; +var IsHigherThan = Ci.nsMsgSearchOp.IsHigherThan; +var IsLowerThan = Ci.nsMsgSearchOp.IsLowerThan; + +var OtherHeader = Ci.nsMsgSearchAttrib.OtherHeader; +var From = Ci.nsMsgSearchAttrib.Sender; +var Subject = Ci.nsMsgSearchAttrib.Subject; +var Priority = Ci.nsMsgSearchAttrib.Priority; +var SDate = Ci.nsMsgSearchAttrib.Date; + +var Tests = [ + // test the To: header + { + testString: "PrimaryEmail1@test.invalid", + testAttribute: From, + op: Is, + count: 1, + }, + { + testString: "PrimaryEmail1@test.invalid", + testAttribute: From, + op: Isnt, + count: 0, + }, + { + testString: "PrimaryEmail", + testAttribute: From, + op: BeginsWith, + count: 1, + }, + { + testString: "invalid", + testAttribute: From, + op: BeginsWith, + count: 0, + }, + { + testString: "invalid", + testAttribute: From, + op: EndsWith, + count: 1, + }, + { + testString: "Primary", + testAttribute: From, + op: EndsWith, + count: 0, + }, + { + testString: "QAContact", + testAttribute: OtherHeader, + op: BeginsWith, + count: 1, + }, + { + testString: "filters", + testAttribute: OtherHeader, + op: BeginsWith, + count: 0, + }, + { + testString: "mail.bugs", + testAttribute: OtherHeader, + op: EndsWith, + count: 1, + }, + { + testString: "QAContact", + testAttribute: OtherHeader, + op: EndsWith, + count: 0, + }, + { + testString: "QAcontact filters@mail.bugs", + testAttribute: OtherHeader, + op: Is, + count: 1, + }, + { + testString: "filters@mail.bugs", + testAttribute: OtherHeader, + op: Is, + count: 0, + }, + { + testString: "QAcontact filters@mail.bugs", + testAttribute: OtherHeader, + op: Isnt, + count: 0, + }, + { + testString: "QAcontact", + testAttribute: OtherHeader, + op: Isnt, + count: 1, + }, + { + testString: "filters", + testAttribute: OtherHeader, + op: Contains, + count: 1, + }, + { + testString: "foobar", + testAttribute: OtherHeader, + op: Contains, + count: 0, + }, + // test header with multiple occurrences + { + testString: "one value", + testAttribute: OtherHeader, + op: Is, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "second", + testAttribute: OtherHeader, + op: Is, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "third value for test purposes", + testAttribute: OtherHeader, + op: Is, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "multiline value that needs to be handled.", + testAttribute: OtherHeader, + op: Is, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "one value", + testAttribute: OtherHeader, + op: Isnt, + customHeader: "X-Duplicated-Header", + count: 0, + }, + { + testString: "second", + testAttribute: OtherHeader, + op: Isnt, + customHeader: "X-Duplicated-Header", + count: 0, + }, + { + testString: "third value for test purposes", + testAttribute: OtherHeader, + op: Isnt, + customHeader: "X-Duplicated-Header", + count: 0, + }, + { + testString: "multiline value that needs to be handled.", + testAttribute: OtherHeader, + op: Isnt, + customHeader: "X-Duplicated-Header", + count: 0, + }, + { + testString: "one", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "second", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "purposes", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "value", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "that needs to be", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "fifth", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "is the end my", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "the end", + testAttribute: OtherHeader, + op: EndsWith, + customHeader: "X-Duplicated-Header", + count: 0, + }, + { + testString: "handled.", + testAttribute: OtherHeader, + op: EndsWith, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "one value", + testAttribute: OtherHeader, + op: EndsWith, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "third", + testAttribute: OtherHeader, + op: BeginsWith, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "This is", + testAttribute: OtherHeader, + op: BeginsWith, + customHeader: "X-Duplicated-Header", + count: 1, + }, + + { + testString: "nothing", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Duplicated-Header", + count: 0, + }, + { + testString: "nothing", + testAttribute: OtherHeader, + op: DoesntContain, + customHeader: "X-Duplicated-Header", + count: 1, + }, + { + testString: "this header tests DB string properties", + testAttribute: OtherHeader, + op: Is, + customHeader: "X-Duplicated-Header-DB", + count: 1, + }, + { + testString: "which can be handled", + testAttribute: OtherHeader, + op: Is, + customHeader: "X-Duplicated-Header-DB", + count: 1, + }, + { + testString: "differently than X-Duplicated-Header, so better test it", + testAttribute: OtherHeader, + op: Is, + customHeader: "X-Duplicated-Header-DB", + count: 1, + }, + { + testString: "this header tests DB string properties", + testAttribute: OtherHeader, + op: Isnt, + customHeader: "X-Duplicated-Header-DB", + count: 0, + }, + { + testString: "which can be handled", + testAttribute: OtherHeader, + op: Isnt, + customHeader: "X-Duplicated-Header-DB", + count: 0, + }, + { + testString: "differently than X-Duplicated-Header, so better test it", + testAttribute: OtherHeader, + op: Isnt, + customHeader: "X-Duplicated-Header-DB", + count: 0, + }, + { + testString: "than X-Duplicated-Header,", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Duplicated-Header-DB", + count: 1, + }, + { + testString: "than X-Duplicated-Header, so", + testAttribute: OtherHeader, + op: DoesntContain, + customHeader: "X-Duplicated-Header-DB", + count: 0, + }, + // test accumulation of received header + { + // only in first received + testString: "caspiaco", + testAttribute: OtherHeader, + op: Contains, + customHeader: "Received", + count: 1, + }, + { + // only in second + testString: "webapp01.sj.mozilla.com", + testAttribute: OtherHeader, + op: Contains, + customHeader: "received", + count: 1, + }, + { + // in neither + testString: "not there", + testAttribute: OtherHeader, + op: Contains, + customHeader: "received", + count: 0, + }, + { + // not on first line of received + testString: "m47LtAFJ007547", + testAttribute: OtherHeader, + op: Contains, + customHeader: "received", + count: 1, + }, + // test multiple line arbitrary headers + { + // in the first line + testString: "SpamAssassin 3.2.3", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Spam-Checker-Version", + count: 1, + }, + { + // in the second line + testString: "host29.example.com", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Spam-Checker-Version", + count: 1, + }, + { + // spans two lines with space + testString: "on host29.example.com", + testAttribute: OtherHeader, + op: Contains, + customHeader: "X-Spam-Checker-Version", + count: 1, + }, + // subject spanning several lines + { + // on the first line + testString: "A filter will", + testAttribute: Subject, + op: Contains, + count: 1, + }, + { + testString: "I do not exist", + testAttribute: Subject, + op: Contains, + count: 0, + }, + { + // on the second line + testString: "this message", + testAttribute: Subject, + op: Contains, + count: 1, + }, + { + // spanning second and third line + testString: "over many", + testAttribute: Subject, + op: Contains, + count: 1, + }, + // tests of custom headers db values + { + testString: "a one line header", + dbHeader: "oneliner", + }, + { + testString: "a two line header", + dbHeader: "twoliner", + }, + { + testString: "a three line header with lotsa space and tabs", + dbHeader: "threeliner", + }, + { + testString: "I have no space", + dbHeader: "nospace", + }, + { + testString: "too much space", + dbHeader: "withspace", + }, + // tests of custom db headers in a search + { + testString: "one line", + testAttribute: OtherHeader, + op: Contains, + customHeader: "oneliner", + count: 1, + }, + { + testString: "two line header", + testAttribute: OtherHeader, + op: Contains, + customHeader: "twoliner", + count: 1, + }, + { + testString: "three line header with lotsa", + testAttribute: OtherHeader, + op: Contains, + customHeader: "threeliner", + count: 1, + }, + { + testString: "I have no space", + testAttribute: OtherHeader, + op: Contains, + customHeader: "nospace", + count: 1, + }, + { + testString: "too much space", + testAttribute: OtherHeader, + op: Contains, + customHeader: "withspace", + count: 1, + }, + // test for priority + { + testString: Ci.nsMsgPriority.lowest, + testAttribute: Priority, + op: IsHigherThan, + count: 1, + }, + { + testString: Ci.nsMsgPriority.low, + testAttribute: Priority, + op: Is, + count: 1, + }, + { + testString: Ci.nsMsgPriority.normal, + testAttribute: Priority, + op: IsLowerThan, + count: 1, + }, + { + testString: Ci.nsMsgPriority.lowest, + testAttribute: Priority, + op: Isnt, + count: 1, + }, + { + testString: Ci.nsMsgPriority.low, + testAttribute: Priority, + op: Isnt, + count: 0, + }, + + // tests of Date header + // The internal value of date in the search is PRTime (nanoseconds since Epoch). + // Date().getTime() returns milliseconds since Epoch. + // The dates used here are tailored for the ../../../data/bugmail12 message. + { + testString: new Date("Wed, 7 May 2008 14:55:10 -0700").getTime() * 1000, + testAttribute: SDate, + op: Is, + count: 1, + }, + { + testString: new Date("Thu, 8 May 2008 14:55:10 -0700").getTime() * 1000, + testAttribute: SDate, + op: IsBefore, + count: 1, + }, + { + testString: new Date("Tue, 6 May 2008 14:55:10 -0700").getTime() * 1000, + testAttribute: SDate, + op: IsAfter, + count: 1, + }, + { + testString: new Date("Tue, 6 May 2008 14:55:10 -0700").getTime() * 1000, + testAttribute: SDate, + op: Isnt, + count: 1, + }, + { + // check bug 248808 + testString: new Date("Wed, 7 May 2008 14:55:10 -0700").getTime() * 1000, + testAttribute: SDate, + op: IsBefore, + count: 0, + }, + { + testString: new Date("Wed, 7 May 2008 14:55:10 -0700").getTime() * 1000, + testAttribute: SDate, + op: IsAfter, + count: 0, + }, +]; + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + + var copyListener = { + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) {}, + SetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + testSearch(); + }, + }; + + // set value of headers we want parsed into the db + Services.prefs.setCharPref( + "mailnews.customDBHeaders", + "oneLiner twoLiner threeLiner noSpace withSpace X-Duplicated-Header-DB" + ); + // Get a message into the local filestore. function testSearch() continues + // the testing after the copy. + var bugmail12 = do_get_file("../../../data/bugmail12"); + do_test_pending(); + MailServices.copy.copyFileMessage( + bugmail12, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + copyListener, + null + ); +} + +// process each test from queue, calls itself upon completion of each search +function testSearch() { + var test = Tests.shift(); + if (test && test.dbHeader) { + // test of a custom db header + dump("testing dbHeader " + test.dbHeader + "\n"); + let customValue = mailTestUtils + .firstMsgHdr(localAccountUtils.inboxFolder) + .getStringProperty(test.dbHeader); + Assert.equal(customValue, test.testString); + do_timeout(0, testSearch); + } else if (test) { + dump("testing for string '" + test.testString + "'\n"); + new TestSearch( + localAccountUtils.inboxFolder, + test.testString, + test.testAttribute, + test.op, + test.count, + testSearch, + null, + test.customHeader ? test.customHeader : "X-Bugzilla-Watch-Reason" + ); + } else { + do_test_finished(); + } +} diff --git a/comm/mailnews/search/test/unit/test_searchAddressInAb.js b/comm/mailnews/search/test/unit/test_searchAddressInAb.js new file mode 100644 index 0000000000..a93f405047 --- /dev/null +++ b/comm/mailnews/search/test/unit/test_searchAddressInAb.js @@ -0,0 +1,337 @@ +/* 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/. */ + +// Testing of to, cc, toorcc in addressbook search features added in bug 187768 +// Added testing of AllAddresses from bug 310359 + +/* import-globals-from ../../../test/resources/searchTestUtils.js */ +load("../../../resources/searchTestUtils.js"); + +// add address book setup +/* import-globals-from ../../../test/resources/abSetup.js */ +load("../../../resources/abSetup.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var ABUri = kPABData.URI; + +var IsntInAB = Ci.nsMsgSearchOp.IsntInAB; +var IsInAB = Ci.nsMsgSearchOp.IsInAB; +var IsBefore = Ci.nsMsgSearchOp.IsBefore; // control entry that is not enabled +var Is = Ci.nsMsgSearchOp.Is; +var Isnt = Ci.nsMsgSearchOp.Isnt; + +var offlineMail = Ci.nsMsgSearchScope.offlineMail; +var onlineMail = Ci.nsMsgSearchScope.onlineMail; +var offlineMailFilter = Ci.nsMsgSearchScope.offlineMailFilter; +var onlineMailFilter = Ci.nsMsgSearchScope.onlineMailFilter; +var news = Ci.nsMsgSearchScope.news; // control entry that is not enabled + +var Sender = Ci.nsMsgSearchAttrib.Sender; +var To = Ci.nsMsgSearchAttrib.To; +var CCopy = Ci.nsMsgSearchAttrib.CC; +var ToOrCC = Ci.nsMsgSearchAttrib.ToOrCC; +var AllAddresses = Ci.nsMsgSearchAttrib.AllAddresses; +var Keywords = Ci.nsMsgSearchAttrib.Keywords; // control entry that is not enabled + +/* + * The address available in the test address book is "PrimaryEmail1@test.invalid" + * Test emails may also include the address "invalid@example.com" + * + * + * Map of test email contents: (P is "Prim...", I is "inva.." address, N is none) + * + * + * Email From To CC BCC + * 1 P I I N + * 2 P P P N + * 3 I P I N + * 4 I I P N + * 5 P I P N + * 6 I I,P P,I N + * 7 I I I P + * 8 I P P N + * + */ + +var Tests = [ + { + value: ABUri, + attrib: Sender, + op: IsInAB, + count: 3, + }, + { + value: ABUri, + attrib: To, + op: IsInAB, + count: 4, + }, + { + value: ABUri, + attrib: ToOrCC, + op: IsInAB, + count: 6, + }, + { + value: ABUri, + attrib: AllAddresses, + op: IsInAB, + count: 8, + }, + { + value: ABUri, + attrib: CCopy, + op: IsInAB, + count: 5, + }, + { + value: ABUri, + attrib: Sender, + op: IsntInAB, + count: 5, + }, + { + value: ABUri, + attrib: To, + op: IsntInAB, + count: 5, + }, + { + value: ABUri, + attrib: ToOrCC, + op: IsntInAB, + count: 6, + }, + { + value: ABUri, + attrib: AllAddresses, + op: IsntInAB, + count: 7, + }, + { + value: ABUri, + attrib: CCopy, + op: IsntInAB, + count: 4, + }, + { + value: "PrimaryEmail1@test.invalid", + attrib: AllAddresses, + op: Is, + count: 8, + }, + { + value: "PrimaryEmail1@test.invalid", + attrib: AllAddresses, + op: Isnt, + count: 0, + }, + { + value: "invalid@example.com", + attrib: AllAddresses, + op: Is, + count: 7, + }, + { + value: "invalid@example.com", + attrib: AllAddresses, + op: Isnt, + count: 1, + }, + { + value: "PrimaryEmail1@test.invalid", + attrib: ToOrCC, + op: Is, + count: 6, + }, + { + value: "PrimaryEmail1@test.invalid", + attrib: ToOrCC, + op: Isnt, + count: 2, + }, + { + value: "invalid@example.com", + attrib: ToOrCC, + op: Is, + count: 6, + }, + { + value: "invalid@example.com", + attrib: ToOrCC, + op: Isnt, + count: 2, + }, +]; + +var Files = [ + "../../../data/bugmail1", + "../../../data/bugmail2", + "../../../data/bugmail3", + "../../../data/bugmail4", + "../../../data/bugmail5", + "../../../data/bugmail6", + "../../../data/bugmail7", + "../../../data/bugmail8", +]; + +function run_test() { + // Setup local mail accounts. + localAccountUtils.loadLocalMailAccount(); + + loadABFile( + "../../../addrbook/test/unit/data/cardForEmail", + kPABData.fileName + ); + + // test that validity table terms are valid + + // offline mail table + testValidityTable(offlineMail, IsInAB, Sender, true); + testValidityTable(offlineMail, IsInAB, To, true); + testValidityTable(offlineMail, IsInAB, ToOrCC, true); + testValidityTable(offlineMail, IsInAB, AllAddresses, true); + testValidityTable(offlineMail, IsInAB, CCopy, true); + testValidityTable(offlineMail, IsInAB, Keywords, false); + testValidityTable(offlineMail, IsntInAB, Sender, true); + testValidityTable(offlineMail, IsntInAB, To, true); + testValidityTable(offlineMail, IsntInAB, ToOrCC, true); + testValidityTable(offlineMail, IsntInAB, AllAddresses, true); + testValidityTable(offlineMail, IsntInAB, CCopy, true); + testValidityTable(offlineMail, IsntInAB, Keywords, false); + testValidityTable(offlineMail, IsBefore, Sender, false); + testValidityTable(offlineMail, IsBefore, To, false); + testValidityTable(offlineMail, IsBefore, ToOrCC, false); + testValidityTable(offlineMail, IsBefore, AllAddresses, false); + testValidityTable(offlineMail, IsBefore, CCopy, false); + testValidityTable(offlineMail, IsBefore, Keywords, false); + testValidityTable(offlineMail, Is, AllAddresses, true); + testValidityTable(offlineMail, Isnt, AllAddresses, true); + + // offline mail filter table + testValidityTable(offlineMailFilter, IsInAB, Sender, true); + testValidityTable(offlineMailFilter, IsInAB, To, true); + testValidityTable(offlineMailFilter, IsInAB, ToOrCC, true); + testValidityTable(offlineMailFilter, IsInAB, AllAddresses, true); + testValidityTable(offlineMailFilter, IsInAB, CCopy, true); + testValidityTable(offlineMailFilter, IsInAB, Keywords, false); + testValidityTable(offlineMailFilter, IsntInAB, Sender, true); + testValidityTable(offlineMailFilter, IsntInAB, To, true); + testValidityTable(offlineMailFilter, IsntInAB, AllAddresses, true); + testValidityTable(offlineMailFilter, IsntInAB, ToOrCC, true); + testValidityTable(offlineMailFilter, IsntInAB, CCopy, true); + testValidityTable(offlineMailFilter, IsntInAB, Keywords, false); + testValidityTable(offlineMailFilter, IsBefore, Sender, false); + testValidityTable(offlineMailFilter, IsBefore, To, false); + testValidityTable(offlineMailFilter, IsBefore, ToOrCC, false); + testValidityTable(offlineMailFilter, IsBefore, AllAddresses, false); + testValidityTable(offlineMailFilter, IsBefore, CCopy, false); + testValidityTable(offlineMailFilter, IsBefore, Keywords, false); + testValidityTable(offlineMailFilter, Is, AllAddresses, true); + testValidityTable(offlineMailFilter, Isnt, AllAddresses, true); + + // online mail + testValidityTable(onlineMail, IsInAB, Sender, false); + testValidityTable(onlineMail, IsInAB, To, false); + testValidityTable(onlineMail, IsInAB, ToOrCC, false); + testValidityTable(onlineMail, IsInAB, CCopy, false); + testValidityTable(onlineMail, IsInAB, Keywords, false); + testValidityTable(onlineMail, IsntInAB, Sender, false); + testValidityTable(onlineMail, IsntInAB, To, false); + testValidityTable(onlineMail, IsntInAB, ToOrCC, false); + testValidityTable(onlineMail, IsntInAB, CCopy, false); + testValidityTable(onlineMail, IsntInAB, Keywords, false); + testValidityTable(onlineMail, IsBefore, Sender, false); + testValidityTable(onlineMail, IsBefore, To, false); + testValidityTable(onlineMail, IsBefore, ToOrCC, false); + testValidityTable(onlineMail, IsBefore, CCopy, false); + testValidityTable(onlineMail, IsBefore, Keywords, false); + + // online mail filter + testValidityTable(onlineMailFilter, IsInAB, Sender, true); + testValidityTable(onlineMailFilter, IsInAB, To, true); + testValidityTable(onlineMailFilter, IsInAB, ToOrCC, true); + testValidityTable(onlineMailFilter, IsInAB, CCopy, true); + testValidityTable(onlineMailFilter, IsInAB, Keywords, false); + testValidityTable(onlineMailFilter, IsntInAB, Sender, true); + testValidityTable(onlineMailFilter, IsntInAB, To, true); + testValidityTable(onlineMailFilter, IsntInAB, ToOrCC, true); + testValidityTable(onlineMailFilter, IsntInAB, CCopy, true); + testValidityTable(onlineMailFilter, IsntInAB, Keywords, false); + testValidityTable(onlineMailFilter, IsBefore, Sender, false); + testValidityTable(onlineMailFilter, IsBefore, To, false); + testValidityTable(onlineMailFilter, IsBefore, ToOrCC, false); + testValidityTable(onlineMailFilter, IsBefore, CCopy, false); + testValidityTable(onlineMailFilter, IsBefore, Keywords, false); + + // news + testValidityTable(news, IsInAB, Sender, false); + testValidityTable(news, IsInAB, To, false); + testValidityTable(news, IsInAB, ToOrCC, false); + testValidityTable(news, IsInAB, CCopy, false); + testValidityTable(news, IsInAB, Keywords, false); + testValidityTable(news, IsntInAB, Sender, false); + testValidityTable(news, IsntInAB, To, false); + testValidityTable(news, IsntInAB, ToOrCC, false); + testValidityTable(news, IsntInAB, CCopy, false); + testValidityTable(news, IsntInAB, Keywords, false); + testValidityTable(news, IsBefore, Sender, false); + testValidityTable(news, IsBefore, To, false); + testValidityTable(news, IsBefore, ToOrCC, false); + testValidityTable(news, IsBefore, CCopy, false); + testValidityTable(news, IsBefore, Keywords, false); + + // Get a message into the local filestore. function testAbSearch() continues the testing after the copy. + do_test_pending(); + copyListener.OnStopCopy(null); + return true; +} + +var copyListener = { + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) {}, + SetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + var fileName = Files.shift(); + if (fileName) { + var file = do_get_file(fileName); + MailServices.copy.copyFileMessage( + file, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + copyListener, + null + ); + } else { + testAbSearch(); + } + }, +}; + +// Runs at completion of copy + +// process each test from queue, calls itself upon completion of each search +function testAbSearch() { + print("Test AbSearch"); + var test = Tests.shift(); + if (test) { + new TestSearch( + localAccountUtils.inboxFolder, + test.value, + test.attrib, + test.op, + test.count, + testAbSearch + ); + } else { + do_test_finished(); + } +} diff --git a/comm/mailnews/search/test/unit/test_searchBody.js b/comm/mailnews/search/test/unit/test_searchBody.js new file mode 100644 index 0000000000..c2ec73c115 --- /dev/null +++ b/comm/mailnews/search/test/unit/test_searchBody.js @@ -0,0 +1,294 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * This tests various body search criteria. + */ +/* import-globals-from ../../../test/resources/searchTestUtils.js */ +load("../../../resources/searchTestUtils.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var Isnt = Ci.nsMsgSearchOp.Isnt; +var Is = Ci.nsMsgSearchOp.Is; +var IsEmpty = Ci.nsMsgSearchOp.IsEmpty; +var IsntEmpty = Ci.nsMsgSearchOp.IsntEmpty; +var Contains = Ci.nsMsgSearchOp.Contains; +var DoesntContain = Ci.nsMsgSearchOp.DoesntContain; +var IsBefore = Ci.nsMsgSearchOp.IsBefore; // control entry not enabled + +var offlineMail = Ci.nsMsgSearchScope.offlineMail; +var onlineMail = Ci.nsMsgSearchScope.onlineMail; +var offlineMailFilter = Ci.nsMsgSearchScope.offlineMailFilter; +var news = Ci.nsMsgSearchScope.news; // control entry not enabled + +var Body = Ci.nsMsgSearchAttrib.Body; + +var Files = [ + "../../../data/base64-1", + "../../../data/basic1", + "../../../data/multipart-base64-2", + "../../../data/bug132340", + "../../../data/bad-charset.eml", + "../../../data/HTML-with-split-tag1.eml", + "../../../data/HTML-with-split-tag2.eml", + + // Base64 encoded bodies. + "../../../data/01-plaintext.eml", + "../../../data/02-plaintext+attachment.eml", + "../../../data/03-HTML.eml", + "../../../data/04-HTML+attachment.eml", + "../../../data/05-HTML+embedded-image.eml", + "../../../data/06-plaintext+HMTL.eml", + "../../../data/07-plaintext+(HTML+embedded-image).eml", + "../../../data/08-plaintext+HTML+attachment.eml", + "../../../data/09-(HTML+embedded-image)+attachment.eml", + "../../../data/10-plaintext+(HTML+embedded-image)+attachment.eml", + + // Bodies with non-ASCII characters in UTF-8 and other charsets. + "../../../data/11-plaintext.eml", + "../../../data/12-plaintext+attachment.eml", // using ISO-8859-7 (Greek) + "../../../data/13-HTML.eml", + "../../../data/14-HTML+attachment.eml", + "../../../data/15-HTML+embedded-image.eml", + "../../../data/16-plaintext+HMTL.eml", // text part is base64 encoded + "../../../data/17-plaintext+(HTML+embedded-image).eml", // HTML part is base64 encoded + "../../../data/18-plaintext+HTML+attachment.eml", + "../../../data/19-(HTML+embedded-image)+attachment.eml", + "../../../data/20-plaintext+(HTML+embedded-image)+attachment.eml", // using windows-1252 + + // Bodies with non-ASCII characters in UTF-8 and other charsets, all encoded with quoted printable. + "../../../data/21-plaintext.eml", + "../../../data/22-plaintext+attachment.eml", // using ISO-8859-7 (Greek) + "../../../data/23-HTML.eml", + "../../../data/24-HTML+attachment.eml", + "../../../data/25-HTML+embedded-image.eml", + "../../../data/26-plaintext+HMTL.eml", // text part is base64 encoded + "../../../data/27-plaintext+(HTML+embedded-image).eml", // HTML part is base64 encoded + "../../../data/28-plaintext+HTML+attachment.eml", + "../../../data/29-(HTML+embedded-image)+attachment.eml", + "../../../data/30-plaintext+(HTML+embedded-image)+attachment.eml", // using windows-1252 + + // Messages with message attachments, Content-Type: message/rfc822. + "../../../data/multipart-message-1.eml", // plaintext, has "bodyOfAttachedMessagePlain" + "../../../data/multipart-message-2.eml", // plaintext, base64, non-ASCII, has "bodyOfAttachedMessagePläin" + "../../../data/multipart-message-3.eml", // plaintext+HTML, non-ASCII in plaintext, has "bodyOfAttachedMessagePläin" + "../../../data/multipart-message-4.eml", // plaintext+HTML, non-ASCII in HTML, has "bodyOfAttachedMessägeHTML" + + // Message using ISO-2022-JP and CTE: quoted-printable. + "../../../data/iso-2022-jp-qp.eml", // plaintext, has 日本 (Japan), we shouldn't find =1B$BF|K. + + // Message using ISO-2022-JP and 7bit, but containing something that looks like quoted-printable. + // (bug 314637). + "../../../data/iso-2022-jp-not-qp.eml", // plaintext, has 現況 which contains =67. +]; +var Tests = [ + /* Translate Base64 messages */ + // "World!" is contained in three messages, but in bug132340 it's not in a text + // part and should not be found. + { value: "World!", op: Contains, count: 2 }, + /* Don't match the base64 text */ + { value: "DQp", op: Contains, count: 0 }, + /* Nested multipart/mixed, don't match */ + { value: "PGh", op: Contains, count: 0 }, + /* An encoded base-64 text/plain match */ + { value: "base 64 text", op: Contains, count: 1 }, + + // From the message with the bad charset. + { value: "Mätterhorn", op: Contains, count: 1 }, + { value: "Matterhörn", op: Contains, count: 1 }, + + // Comprehensive test of various MIME structures, messages 01 to 10. + // Messages 01 to 10 contain "huhu" once. + { value: "huhu", op: Contains, count: 10 }, + + // Messages 06, 07, 08, 10 contain "hihi" in the plaintext part. + { value: "hihi", op: Contains, count: 4 }, + + // The base64 of embedded images and attachments contains "iVBORw" and we don't + // want to find that. + { value: "iVBORw", op: Contains, count: 0 }, + + // The base64 of attachments contains "wMA005J0z" and we don't want to find that. + { value: "wMA005J0z", op: Contains, count: 0 }, + + // The base64 of the plaintext and HTML parts contains "U2VhcmNoIGZ" + // and we don't want to find that. + { value: "U2VhcmNoIGZ", op: Contains, count: 0 }, + + // Messages 11 and 13 to 20 contain "hühü" once. + { value: "hühü", op: Contains, count: 9 }, + // Message 12 contains Καλησπέρα (good evening in Greek). + { value: "Καλησπέρα", op: Contains, count: 1 }, + + // Messages 16, 17, 18, 20 contain "hïhï" in the plaintext part. + { value: "hïhï", op: Contains, count: 4 }, + + // Messages 21 and 23 to 30 contain "höhö" once. + { value: "höhö", op: Contains, count: 9 }, + // Message 22 contains Καλημέρα (good morning in Greek). + { value: "Καλημέρα", op: Contains, count: 1 }, + + // Messages 21, 23 and 24 contain "softbreak" broken by a soft line break. + { value: "softbreak", op: Contains, count: 3 }, + + // Messages 16, 17, 18, 20 contain "hähä" in the plaintext part. + { value: "hähä", op: Contains, count: 4 }, + + // The four messages with message/rfc822 attachment contain "bodyOfAttachedMessagePlain" + // or "bodyOfAttachedMessagePläin" in the plaintext part and "bodyOfAttachedMessageHTML" + // or "bodyOfAttachedMessägeHTML" in the HTML part. + { value: "bodyOfAttachedMessagePlain", op: Contains, count: 2 }, + { value: "bodyOfAttachedMessagePläin", op: Contains, count: 2 }, + { value: "bodyOfAttachedMessageHTML", op: Contains, count: 1 }, + { value: "bodyOfAttachedMessägeHTML", op: Contains, count: 1 }, + + // Test that we don't find anything in HTML tags. + { value: "ShouldNotFindThis", op: Contains, count: 0 }, + { value: "ShouldntFindThisEither", op: Contains, count: 0 }, + { value: "ShouldntFindHref", op: Contains, count: 0 }, + { value: "ShouldNotFindAcrossLines", op: Contains, count: 0 }, + { value: "ShouldFindThisAgain", op: Contains, count: 2 }, + { value: "ShouldFind AcrossLines", op: Contains, count: 2 }, + + // Test for ISO-2022-JP and CTE: quoted-printable, also 7bit looking like quoted-printable. + { value: "日本", op: Contains, count: 1 }, + { value: "=1B$BF|K", op: Contains, count: 0 }, + { value: "現況", op: Contains, count: 1 }, +]; + +function fixFile(file) { + var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(file, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF); + var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + sstream.init(fstream); + + var str = sstream.read(4096); + if (str.startsWith("From ")) { + sstream.close(); + fstream.close(); + return file; + } + var data = "From - Tue Oct 02 00:26:47 2007\r\n"; + do { + data += str; + str = sstream.read(4096); + } while (str.length > 0); + + sstream.close(); + fstream.close(); + + let targetFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + targetFile.initWithFile(do_get_profile()); + targetFile.append(file.leafName); + let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance( + Ci.nsIFileOutputStream + ); + ostream.init(targetFile, -1, -1, 0); + ostream.write(data, data.length); + ostream.close(); + return targetFile; +} + +var copyListener = { + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) {}, + SetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + var fileName = Files.shift(); + if (fileName) { + var file = fixFile(do_get_file(fileName)); + MailServices.copy.copyFileMessage( + file, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + copyListener, + null + ); + } else { + testBodySearch(); + } + }, +}; + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + + // test that validity table terms are valid + + // offline mail table + testValidityTable(offlineMail, Contains, Body, true); + testValidityTable(offlineMail, DoesntContain, Body, true); + testValidityTable(offlineMail, Is, Body, true); + testValidityTable(offlineMail, Isnt, Body, true); + testValidityTable(offlineMail, IsEmpty, Body, false); + testValidityTable(offlineMail, IsntEmpty, Body, false); + testValidityTable(offlineMail, IsBefore, Body, false); + + // offline mail filter table + testValidityTable(offlineMailFilter, Contains, Body, true); + testValidityTable(offlineMailFilter, DoesntContain, Body, true); + testValidityTable(offlineMailFilter, Is, Body, true); + testValidityTable(offlineMailFilter, Isnt, Body, true); + testValidityTable(offlineMailFilter, IsEmpty, Body, false); + testValidityTable(offlineMailFilter, IsntEmpty, Body, false); + testValidityTable(offlineMailFilter, IsBefore, Body, false); + + // online mail + testValidityTable(onlineMail, Contains, Body, true); + testValidityTable(onlineMail, DoesntContain, Body, true); + testValidityTable(onlineMail, Is, Body, false); + testValidityTable(onlineMail, Isnt, Body, false); + testValidityTable(onlineMail, IsEmpty, Body, false); + testValidityTable(onlineMail, IsntEmpty, Body, false); + testValidityTable(onlineMail, IsBefore, Body, false); + + // online mail filter + /* testValidityTable(onlineMailFilter, Contains, Body, true); + testValidityTable(onlineMailFilter, DoesntContain, Body, true); + testValidityTable(onlineMailFilter, Is, Body, false); + testValidityTable(onlineMailFilter, Isnt, Body, false); + testValidityTable(onlineMailFilter, IsEmpty, Body, false); + testValidityTable(onlineMailFilter, IsntEmpty, Body, false); + testValidityTable(onlineMailFilter, IsBefore, Body, false);*/ + + // News does not support body tests + testValidityTable(news, Contains, Body, false); + testValidityTable(news, DoesntContain, Body, false); + testValidityTable(news, Is, Body, false); + testValidityTable(news, Isnt, Body, false); + testValidityTable(news, IsEmpty, Body, false); + testValidityTable(news, IsntEmpty, Body, false); + testValidityTable(news, IsBefore, Body, false); + + do_test_pending(); + copyListener.OnStopCopy(null); +} + +// process each test from queue, calls itself upon completion of each search +function testBodySearch() { + var test = Tests.shift(); + if (test) { + new TestSearch( + localAccountUtils.inboxFolder, + test.value, + Body, + test.op, + test.count, + testBodySearch + ); + } else { + do_test_finished(); + } +} diff --git a/comm/mailnews/search/test/unit/test_searchBoolean.js b/comm/mailnews/search/test/unit/test_searchBoolean.js new file mode 100644 index 0000000000..132acad4a3 --- /dev/null +++ b/comm/mailnews/search/test/unit/test_searchBoolean.js @@ -0,0 +1,239 @@ +/* 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/. */ + +/* + * Demonstrates and tests the use of grouped boolean expressions in search terms + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var gSearchSession = Cc[ + "@mozilla.org/messenger/searchSession;1" +].createInstance(Ci.nsIMsgSearchSession); + +var gHdr; // the message header for the one mailbox message + +var Tests = [ + { + A: false, + B: false, + C: false, + D: false, + matches: false, + }, + { + A: false, + B: false, + C: false, + D: true, + matches: false, + }, + { + A: false, + B: false, + C: true, + D: false, + matches: false, + }, + { + A: false, + B: false, + C: true, + D: true, + matches: false, + }, + { + A: false, + B: true, + C: false, + D: false, + matches: false, + }, + { + A: false, + B: true, + C: false, + D: true, + matches: true, + }, + { + A: false, + B: true, + C: true, + D: false, + matches: true, + }, + { + A: false, + B: true, + C: true, + D: true, + matches: true, + }, + { + A: true, + B: false, + C: false, + D: false, + matches: false, + }, + { + A: true, + B: false, + C: false, + D: true, + matches: true, + }, + { + A: true, + B: false, + C: true, + D: false, + matches: true, + }, + { + A: true, + B: false, + C: true, + D: true, + matches: true, + }, + { + A: true, + B: true, + C: false, + D: false, + matches: false, + }, + { + A: true, + B: true, + C: false, + D: true, + matches: true, + }, + { + A: true, + B: true, + C: true, + D: false, + matches: true, + }, + { + A: true, + B: true, + C: true, + D: true, + matches: true, + }, +]; + +var gHitCount = 0; +var searchListener = { + onSearchHit(dbHdr, folder) { + gHitCount++; + }, + onSearchDone(status) { + testSearch(); + }, + onNewSearch() { + gHitCount = 0; + }, +}; + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + + /* + * I want to create and test a search term that uses this expression: + * (A || B ) && (C || D) + * + * The logical expressions A, B, C, and D will be represented by header + * string properties with values of T (true) or F (false). + * + */ + + // add a search term HdrProperty (some string) Is "T", with grouping + function addSearchTerm(aHdrProperty, aBeginGrouping, aEndGrouping, aBoolAnd) { + let searchTerm = gSearchSession.createTerm(); + searchTerm.attrib = Ci.nsMsgSearchAttrib.HdrProperty; + + let value = searchTerm.value; + // This is tricky - value.attrib must be set before actual values + value.attrib = Ci.nsMsgSearchAttrib.HdrProperty; + value.str = "T"; + searchTerm.value = value; + + searchTerm.op = Ci.nsMsgSearchOp.Is; + searchTerm.booleanAnd = aBoolAnd; + searchTerm.beginsGrouping = aBeginGrouping; + searchTerm.endsGrouping = aEndGrouping; + searchTerm.hdrProperty = aHdrProperty; + gSearchSession.appendTerm(searchTerm); + } + + gSearchSession.addScopeTerm( + Ci.nsMsgSearchScope.offlineMail, + localAccountUtils.inboxFolder + ); + gSearchSession.registerListener(searchListener); + // I tried using capital "A" but something makes it lower case internally, so it failed + addSearchTerm("a", true, false, true); // "(A" + addSearchTerm("b", false, true, false); // " || B)" + addSearchTerm("c", true, false, true); // " && (C" + addSearchTerm("d", false, true, false); // " || D)" + + var copyListener = { + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) { + gHdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey); + }, + SetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + testSearch(); + }, + }; + + // Get a message into the local filestore. function testSearch() continues + // the testing after the copy. + var bugmail1 = do_get_file("../../../data/bugmail1"); + do_test_pending(); + MailServices.copy.copyFileMessage( + bugmail1, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + copyListener, + null + ); +} + +var gTest = null; +// process each test from queue, calls itself upon completion of each search +function testSearch() { + // tests the previous search + if (gTest) { + Assert.equal(gHitCount, gTest.matches ? 1 : 0); + } + gTest = Tests.shift(); + if (gTest) { + gHdr.setStringProperty("a", gTest.A ? "T" : "F"); + gHdr.setStringProperty("b", gTest.B ? "T" : "F"); + gHdr.setStringProperty("c", gTest.C ? "T" : "F"); + gHdr.setStringProperty("d", gTest.D ? "T" : "F"); + try { + gSearchSession.search(null); + } catch (e) { + dump(e); + } + } else { + gSearchSession.unregisterListener(searchListener); + do_test_finished(); + } +} diff --git a/comm/mailnews/search/test/unit/test_searchChaining.js b/comm/mailnews/search/test/unit/test_searchChaining.js new file mode 100644 index 0000000000..4bdbebab57 --- /dev/null +++ b/comm/mailnews/search/test/unit/test_searchChaining.js @@ -0,0 +1,90 @@ +/* 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/. */ + +// Test of chaining of search scopes in a search session. In particular, +// Bug 541969 made us not search an imap folder if the search scope before it +// there was an empty local folder. + +// main test + +/* import-globals-from ../../../test/resources/MessageGenerator.jsm */ +load("../../../resources/MessageGenerator.jsm"); + +var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import( + "resource://testing-common/mailnews/IMAPpump.jsm" +); +var { ImapMessage } = ChromeUtils.import( + "resource://testing-common/mailnews/Imapd.jsm" +); +const { PromiseTestUtils } = ChromeUtils.import( + "resource://testing-common/mailnews/PromiseTestUtils.jsm" +); + +async function setupFolder() { + // add a single message to the imap inbox. + let messages = []; + let messageGenerator = new MessageGenerator(); + messages = messages.concat(messageGenerator.makeMessage()); + let synthMessage = messages[0]; + + let msgURI = Services.io.newURI( + "data:text/plain;base64," + btoa(synthMessage.toMessageString()) + ); + let message = new ImapMessage(msgURI.spec, IMAPPump.mailbox.uidnext++, []); + IMAPPump.mailbox.addMessage(message); + + // update folder to download header. + let listener = new PromiseTestUtils.PromiseUrlListener(); + IMAPPump.inbox.updateFolderWithListener(null, listener); + await listener.promise; +} + +async function searchTest() { + // Get the IMAP inbox... + var emptyLocal1 = + localAccountUtils.rootFolder.createLocalSubfolder("empty 1"); + + let searchSession = Cc[ + "@mozilla.org/messenger/searchSession;1" + ].createInstance(Ci.nsIMsgSearchSession); + + let searchTerm = searchSession.createTerm(); + searchTerm.matchAll = true; + searchSession.appendTerm(searchTerm); + searchSession.addScopeTerm(Ci.nsMsgSearchScope.offlineMail, emptyLocal1); + searchSession.addScopeTerm(Ci.nsMsgSearchScope.onlineMail, IMAPPump.inbox); + let listener = new PromiseTestUtils.PromiseSearchNotify( + searchSession, + searchListener + ); + searchSession.search(null); + await listener.promise; + + // After the search completes, there still seem to be active URLs, so we + // have to wait before we are done and clear. + await PromiseTestUtils.promiseDelay(1000); +} + +// nsIMsgSearchNotify implementation +var searchListener = { + numTotalMessages: 0, + QueryInterface: ChromeUtils.generateQI(["nsIMsgSearchNotify"]), + onNewSearch() { + this.numTotalMessages = 0; + }, + onSearchHit(dbHdr, folder) { + this.numTotalMessages++; + }, + onSearchDone(status) { + Assert.equal(this.numTotalMessages, 1); + return true; + }, +}; + +var tests = [setupIMAPPump, setupFolder, searchTest, teardownIMAPPump]; + +function run_test() { + tests.forEach(x => add_task(x)); + run_next_test(); +} diff --git a/comm/mailnews/search/test/unit/test_searchCustomTerm.js b/comm/mailnews/search/test/unit/test_searchCustomTerm.js new file mode 100644 index 0000000000..baea69767b --- /dev/null +++ b/comm/mailnews/search/test/unit/test_searchCustomTerm.js @@ -0,0 +1,112 @@ +/* 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/. */ + +/* + * Testing of custom search features. + * + */ +/* import-globals-from ../../../test/resources/searchTestUtils.js */ +load("../../../resources/searchTestUtils.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var kCustomId = "xpcomtest@mozilla.org#test"; +var gHdr; + +var Tests = [ + { + setValue: "iamgood", + testValue: "iamnotgood", + op: Ci.nsMsgSearchOp.Is, + count: 0, + }, + { + setValue: "iamgood", + testValue: "iamgood", + op: Ci.nsMsgSearchOp.Is, + count: 1, + }, +]; + +// nsIMsgSearchCustomTerm object +var customTerm = { + id: kCustomId, + name: "term name", + getEnabled(scope, op) { + return ( + scope == Ci.nsMsgSearchScope.offlineMail && op == Ci.nsMsgSearchOp.Is + ); + }, + getAvailable(scope, op) { + return ( + scope == Ci.nsMsgSearchScope.offlineMail && op == Ci.nsMsgSearchOp.Is + ); + }, + getAvailableOperators(scope) { + return [Ci.nsMsgSearchOp.Is]; + }, + match(msgHdr, searchValue, searchOp) { + switch (searchOp) { + case Ci.nsMsgSearchOp.Is: + if (msgHdr.getStringProperty("theTestProperty") == searchValue) { + return true; + } + } + return false; + }, +}; + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + MailServices.filters.addCustomTerm(customTerm); + + var copyListener = { + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) { + gHdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey); + }, + SetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + doTest(); + }, + }; + + // Get a message into the local filestore. + // function testSearch() continues the testing after the copy. + let bugmail1 = do_get_file("../../../data/bugmail1"); + do_test_pending(); + + MailServices.copy.copyFileMessage( + bugmail1, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + copyListener, + null + ); +} + +function doTest() { + let test = Tests.shift(); + if (test) { + gHdr.setStringProperty("theTestProperty", test.setValue); + new TestSearch( + localAccountUtils.inboxFolder, + test.testValue, + Ci.nsMsgSearchAttrib.Custom, + test.op, + test.count, + doTest, + kCustomId + ); + } else { + gHdr = null; + do_test_finished(); + } +} diff --git a/comm/mailnews/search/test/unit/test_searchJunk.js b/comm/mailnews/search/test/unit/test_searchJunk.js new file mode 100644 index 0000000000..fdffd96165 --- /dev/null +++ b/comm/mailnews/search/test/unit/test_searchJunk.js @@ -0,0 +1,322 @@ +/* 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/. */ + +// Testing of search by junk percent and junk score origin + +/* import-globals-from ../../../test/resources/searchTestUtils.js */ +load("../../../resources/searchTestUtils.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var IsGreaterThan = Ci.nsMsgSearchOp.IsGreaterThan; +var IsLessThan = Ci.nsMsgSearchOp.IsLessThan; +var Is = Ci.nsMsgSearchOp.Is; +var Isnt = Ci.nsMsgSearchOp.Isnt; +var IsEmpty = Ci.nsMsgSearchOp.IsEmpty; +var IsntEmpty = Ci.nsMsgSearchOp.IsntEmpty; + +var offlineMail = Ci.nsMsgSearchScope.offlineMail; + +var JunkScoreOrigin = Ci.nsMsgSearchAttrib.JunkScoreOrigin; +var JunkPercent = Ci.nsMsgSearchAttrib.JunkPercent; +var JunkStatus = Ci.nsMsgSearchAttrib.JunkStatus; + +var fileName = "../../../data/bugmail1"; + +/* + * The search for junkpercent is defined as the effective value, + * while the "junkpercent" database term is always the result + * from the bayes filter. This is optimized to make views that + * rely on junk percent search work with the best value for junk + * percent, while allowing junk percent from the bayes filter + * to be saved for analysis. + * + * This means that the search for "junk percent" only uses the + * database junkpercent value if junkscoreorigin is "plugin". + * Otherwise, it uses junkstatus (if set) or defaults to 0 + * (not junk) if the message is unclassified. + */ + +var Tests = [ + { + // test empty junk status + junkScore: false, + testValue: 90, + attrib: JunkStatus, + op: IsEmpty, + count: 1, + }, + { + junkScore: false, + testValue: 90, + attrib: JunkStatus, + op: IsntEmpty, + count: 0, + }, + { + junkScore: "0", + junkScoreOrigin: "plugin", + junkPercent: "10", + testValue: 90, + attrib: JunkStatus, + op: IsntEmpty, + count: 1, + }, + { + junkScore: "0", + junkScoreOrigin: "plugin", + junkPercent: "10", + testValue: 90, + attrib: JunkStatus, + op: IsEmpty, + count: 0, + }, + { + junkScore: "100", + junkScoreOrigin: "plugin", + junkPercent: "10", + testValue: 90, + attrib: JunkStatus, + op: IsntEmpty, + count: 1, + }, + { + junkScore: "100", + junkScoreOrigin: "plugin", + junkPercent: "10", + testValue: 90, + attrib: JunkStatus, + op: IsEmpty, + count: 0, + }, + { + // Use junkpercent from database + junkScore: "0", + junkScoreOrigin: "plugin", + junkPercent: "10", + testValue: 90, + attrib: JunkPercent, + op: IsGreaterThan, + count: 0, + }, + { + junkScore: "0", + junkScoreOrigin: "plugin", + junkPercent: "10", + testValue: 90, + attrib: JunkPercent, + op: IsLessThan, + count: 1, + }, + { + junkScore: "0", + junkScoreOrigin: "plugin", + junkPercent: "10", + testValue: 90, + attrib: JunkPercent, + op: Is, + count: 0, + }, + { + junkScore: "0", + junkScoreOrigin: "plugin", + junkPercent: "90", + testValue: 10, + attrib: JunkPercent, + op: IsGreaterThan, + count: 1, + }, + { + junkScore: "0", + junkScoreOrigin: "plugin", + junkPercent: "90", + testValue: 10, + attrib: JunkPercent, + op: IsLessThan, + count: 0, + }, + { + junkScore: "0", + junkScoreOrigin: "plugin", + junkPercent: "10", + testValue: 10, + attrib: JunkPercent, + op: Is, + count: 1, + }, + { + // values set by user, use junkscore not junkpercent + junkScore: "0", + junkScoreOrigin: "user", + junkPercent: "90", + testValue: 50, + attrib: JunkPercent, + op: IsGreaterThan, + count: 0, + }, + { + junkScore: "0", + junkScoreOrigin: "user", + junkPercent: "90", + testValue: 50, + attrib: JunkPercent, + op: IsLessThan, + count: 1, + }, + { + junkScore: "0", + junkScoreOrigin: "user", + junkPercent: "90", + testValue: 50, + attrib: JunkPercent, + op: Is, + count: 0, + }, + { + junkScore: "100", + junkScoreOrigin: "user", + junkPercent: "10", + testValue: 50, + attrib: JunkPercent, + op: IsGreaterThan, + count: 1, + }, + { + junkScore: "100", + junkScoreOrigin: "user", + junkPercent: "10", + testValue: 50, + attrib: JunkPercent, + op: IsLessThan, + count: 0, + }, + { + junkScore: "0", + junkScoreOrigin: "user", + junkPercent: "90", + testValue: 0, + attrib: JunkPercent, + op: Is, + count: 1, + }, + { + // default to 0 when nothing set + junkScore: "", + junkScoreOrigin: "", + junkPercent: "", + testValue: 0, + attrib: JunkPercent, + op: Is, + count: 1, + }, + { + // junkscoreorigin search tests + junkScore: "0", + junkScoreOrigin: "plugin", + junkPercent: "50", + testValue: "plugin", + attrib: JunkScoreOrigin, + op: Is, + count: 1, + }, + { + junkScore: "0", + junkScoreOrigin: "plugin", + junkPercent: "50", + testValue: "plugin", + attrib: JunkScoreOrigin, + op: Isnt, + count: 0, + }, + { + junkScore: "0", + junkScoreOrigin: "filter", + junkPercent: "50", + testValue: "plugin", + attrib: JunkScoreOrigin, + op: Is, + count: 0, + }, + { + junkScore: "0", + junkScoreOrigin: "filter", + junkPercent: "50", + testValue: "plugin", + attrib: JunkScoreOrigin, + op: Isnt, + count: 1, + }, +]; + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + + // test that validity table terms are valid + + // offline mail table + testValidityTable(offlineMail, Is, JunkPercent, true); + testValidityTable(offlineMail, Isnt, JunkPercent, false); + testValidityTable(offlineMail, IsGreaterThan, JunkPercent, true); + testValidityTable(offlineMail, IsLessThan, JunkPercent, true); + + testValidityTable(offlineMail, Is, JunkScoreOrigin, true); + testValidityTable(offlineMail, Isnt, JunkScoreOrigin, true); + testValidityTable(offlineMail, IsGreaterThan, JunkScoreOrigin, false); + testValidityTable(offlineMail, IsLessThan, JunkScoreOrigin, false); + + // Get a message into the local filestore. function testJunkSearch() continues the testing after the copy. + do_test_pending(); + var file = do_get_file(fileName); + MailServices.copy.copyFileMessage( + file, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + copyListener, + null + ); + return true; +} + +var hdr; +var copyListener = { + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) { + hdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey); + }, + SetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + testJunkSearch(); + }, +}; + +// Runs at completion of each copy +// process each test from queue, calls itself upon completion of each search +function testJunkSearch() { + var test = Tests.shift(); + if (test) { + if (test.junkScore) { + hdr.setStringProperty("junkpercent", test.junkPercent); + hdr.setStringProperty("junkscoreorigin", test.junkScoreOrigin); + hdr.setStringProperty("junkscore", test.junkScore); + } + + new TestSearch( + localAccountUtils.inboxFolder, + test.testValue, + test.attrib, + test.op, + test.count, + testJunkSearch + ); + } else { + hdr = null; + do_test_finished(); + } +} diff --git a/comm/mailnews/search/test/unit/test_searchLocalizationStrings.js b/comm/mailnews/search/test/unit/test_searchLocalizationStrings.js new file mode 100644 index 0000000000..9de9b7eb0e --- /dev/null +++ b/comm/mailnews/search/test/unit/test_searchLocalizationStrings.js @@ -0,0 +1,61 @@ +/* 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/. */ + +// tests that localization strings added in bug 484147 are defined in preferences + +var gValidityManager = Cc[ + "@mozilla.org/mail/search/validityManager;1" +].getService(Ci.nsIMsgSearchValidityManager); + +var gStringBundle = Services.strings.createBundle( + "chrome://messenger/locale/search-attributes.properties" +); + +// The following table of valid table scopes matches the allowable table +// scopes in nsMsgSearchValidityManager::GetTable +var kValidScopes = [ + Ci.nsMsgSearchScope.offlineMail, + Ci.nsMsgSearchScope.offlineMailFilter, + Ci.nsMsgSearchScope.onlineMail, + Ci.nsMsgSearchScope.onlineMailFilter, + Ci.nsMsgSearchScope.news, + Ci.nsMsgSearchScope.newsFilter, + Ci.nsMsgSearchScope.localNews, + Ci.nsMsgSearchScope.LDAP, + Ci.nsMsgSearchScope.LDAPAnd, + Ci.nsMsgSearchScope.LocalAB, + Ci.nsMsgSearchScope.LocalABAnd, +]; + +function run_test() { + for (var index = 0; index < kValidScopes.length; ++index) { + let scope = kValidScopes[index]; + let table = gValidityManager.getTable(scope); + let attributes = table.getAvailableAttributes(); + let attribute; + while ((attribute = attributes.pop()) && attribute) { + let property = gValidityManager.getAttributeProperty(attribute); + let valid = false; + let localizedString; + try { + localizedString = gStringBundle.GetStringFromName(property); + valid = true; + } catch (e) { + dump("\n" + e); + } + valid = valid && localizedString && localizedString.length > 0; + if (!valid) { + dump( + "\nNo valid property for scope = " + + scope + + " attribute = " + + attribute + + " property = " + + property + ); + } + Assert.ok(valid); + } + } +} diff --git a/comm/mailnews/search/test/unit/test_searchTag.js b/comm/mailnews/search/test/unit/test_searchTag.js new file mode 100644 index 0000000000..321c22a6cf --- /dev/null +++ b/comm/mailnews/search/test/unit/test_searchTag.js @@ -0,0 +1,490 @@ +/* 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/. */ + +/* + * Testing of tag search features. + * + * Specifically tests changes implemented in bug 217034 + * Does not do comprehensive testing. + * + */ +/* import-globals-from ../../../test/resources/searchTestUtils.js */ +load("../../../resources/searchTestUtils.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var Isnt = Ci.nsMsgSearchOp.Isnt; +var Is = Ci.nsMsgSearchOp.Is; +var IsEmpty = Ci.nsMsgSearchOp.IsEmpty; +var IsntEmpty = Ci.nsMsgSearchOp.IsntEmpty; +var Contains = Ci.nsMsgSearchOp.Contains; +var DoesntContain = Ci.nsMsgSearchOp.DoesntContain; +var IsBefore = Ci.nsMsgSearchOp.IsBefore; // control entry not enabled + +var offlineMail = Ci.nsMsgSearchScope.offlineMail; +var onlineMail = Ci.nsMsgSearchScope.onlineMail; +var offlineMailFilter = Ci.nsMsgSearchScope.offlineMailFilter; +var onlineMailFilter = Ci.nsMsgSearchScope.onlineMailFilter; +var news = Ci.nsMsgSearchScope.news; // control entry not enabled + +var Keywords = Ci.nsMsgSearchAttrib.Keywords; + +// test tags +var Tag1 = "istag"; +var Tag2 = "notistag"; +var Tag3 = "istagnot"; +var Tag4 = "istagtoo"; +var Tag1Tag4 = Tag1 + " " + Tag4; +var Tag1Tag3 = Tag1 + " " + Tag3; +var Tag1Tag1 = Tag1 + " " + Tag1; + +var Tests = [ + // Message has a single valid tag + // test the valid tag + { + msgTag: Tag1, + testTag: Tag1, + op: Is, + count: 1, + }, + { + msgTag: Tag1, + testTag: Tag1, + op: Isnt, + count: 0, + }, + { + msgTag: Tag1, + testTag: Tag1, + op: Contains, + count: 1, + }, + { + msgTag: Tag1, + testTag: Tag1, + op: DoesntContain, + count: 0, + }, + { + msgTag: Tag1, + testTag: Tag1, + op: IsEmpty, + count: 0, + }, + { + msgTag: Tag1, + testTag: Tag1, + op: IsntEmpty, + count: 1, + }, + // test an invalid tag, should act like empty + { + msgTag: Tag2, + testTag: Tag1, + op: Contains, + count: 0, + }, + { + msgTag: Tag2, + testTag: Tag1, + op: DoesntContain, + count: 1, + }, + { + msgTag: Tag2, + testTag: Tag1, + op: Is, + count: 0, + }, + { + msgTag: Tag2, + testTag: Tag1, + op: Isnt, + count: 1, + }, + { + msgTag: Tag2, + testTag: Tag1, + op: IsEmpty, + count: 1, + }, + { + msgTag: Tag2, + testTag: Tag1, + op: IsntEmpty, + count: 0, + }, + // Message has two valid tags + // test first tag + { + msgTag: Tag1Tag4, + testTag: Tag1, + op: Is, + count: 0, + }, + { + msgTag: Tag1Tag4, + testTag: Tag1, + op: Isnt, + count: 1, + }, + { + msgTag: Tag1Tag4, + testTag: Tag1, + op: Contains, + count: 1, + }, + { + msgTag: Tag1Tag4, + testTag: Tag1, + op: DoesntContain, + count: 0, + }, + { + msgTag: Tag1Tag4, + testTag: Tag1, + op: IsEmpty, + count: 0, + }, + { + msgTag: Tag1Tag4, + testTag: Tag1, + op: IsntEmpty, + count: 1, + }, + // test second tag + { + msgTag: Tag1Tag4, + testTag: Tag4, + op: Is, + count: 0, + }, + { + msgTag: Tag1Tag4, + testTag: Tag4, + op: Isnt, + count: 1, + }, + { + msgTag: Tag1Tag4, + testTag: Tag4, + op: Contains, + count: 1, + }, + { + msgTag: Tag1Tag4, + testTag: Tag4, + op: DoesntContain, + count: 0, + }, + { + msgTag: Tag1Tag4, + testTag: Tag4, + op: IsEmpty, + count: 0, + }, + { + msgTag: Tag1Tag4, + testTag: Tag4, + op: IsntEmpty, + count: 1, + }, + // test tag not in message + { + msgTag: Tag1Tag4, + testTag: Tag2, + op: Is, + count: 0, + }, + { + msgTag: Tag1Tag4, + testTag: Tag2, + op: Isnt, + count: 1, + }, + { + msgTag: Tag1Tag4, + testTag: Tag2, + op: Contains, + count: 0, + }, + { + msgTag: Tag1Tag4, + testTag: Tag2, + op: DoesntContain, + count: 1, + }, + { + msgTag: Tag1Tag4, + testTag: Tag2, + op: IsEmpty, + count: 0, + }, + { + msgTag: Tag1Tag4, + testTag: Tag2, + op: IsntEmpty, + count: 1, + }, + // empty message + { + msgTag: "", + testTag: Tag2, + op: Is, + count: 0, + }, + { + msgTag: "", + testTag: Tag2, + op: Isnt, + count: 1, + }, + { + msgTag: "", + testTag: Tag2, + op: Contains, + count: 0, + }, + { + msgTag: "", + testTag: Tag2, + op: DoesntContain, + count: 1, + }, + { + msgTag: "", + testTag: Tag2, + op: IsEmpty, + count: 1, + }, + { + msgTag: "", + testTag: Tag2, + op: IsntEmpty, + count: 0, + }, + // message with two tags, only one is valid + // test with the single valid tag + { + msgTag: Tag1Tag3, + testTag: Tag1, + op: Is, + count: 1, + }, + { + msgTag: Tag1Tag3, + testTag: Tag1, + op: Isnt, + count: 0, + }, + { + msgTag: Tag1Tag3, + testTag: Tag1, + op: Contains, + count: 1, + }, + { + msgTag: Tag1Tag3, + testTag: Tag1, + op: DoesntContain, + count: 0, + }, + { + msgTag: Tag1Tag3, + testTag: Tag1, + op: IsEmpty, + count: 0, + }, + { + msgTag: Tag1Tag3, + testTag: Tag1, + op: IsntEmpty, + count: 1, + }, + // test with a tag not in the message + { + msgTag: Tag1Tag3, + testTag: Tag2, + op: Is, + count: 0, + }, + { + msgTag: Tag1Tag3, + testTag: Tag2, + op: Isnt, + count: 1, + }, + { + msgTag: Tag1Tag3, + testTag: Tag2, + op: Contains, + count: 0, + }, + { + msgTag: Tag1Tag3, + testTag: Tag2, + op: DoesntContain, + count: 1, + }, + { + msgTag: Tag1Tag3, + testTag: Tag2, + op: IsEmpty, + count: 0, + }, + { + msgTag: Tag1Tag3, + testTag: Tag2, + op: IsntEmpty, + count: 1, + }, + // Message has a duplicated tag + // test the tag + { + msgTag: Tag1Tag1, + testTag: Tag1, + op: Is, + count: 1, + }, + { + msgTag: Tag1Tag1, + testTag: Tag1, + op: Isnt, + count: 0, + }, + { + msgTag: Tag1Tag1, + testTag: Tag1, + op: Contains, + count: 1, + }, + { + msgTag: Tag1Tag1, + testTag: Tag1, + op: DoesntContain, + count: 0, + }, + { + msgTag: Tag1Tag1, + testTag: Tag1, + op: IsEmpty, + count: 0, + }, + { + msgTag: Tag1Tag1, + testTag: Tag1, + op: IsntEmpty, + count: 1, + }, +]; + +var hdr; + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + + // test that validity table terms are valid + + // offline mail table + testValidityTable(offlineMail, Contains, Keywords, true); + testValidityTable(offlineMail, DoesntContain, Keywords, true); + testValidityTable(offlineMail, Is, Keywords, true); + testValidityTable(offlineMail, Isnt, Keywords, true); + testValidityTable(offlineMail, IsEmpty, Keywords, true); + testValidityTable(offlineMail, IsntEmpty, Keywords, true); + testValidityTable(offlineMail, IsBefore, Keywords, false); + + // offline mail filter table + testValidityTable(offlineMailFilter, Contains, Keywords, true); + testValidityTable(offlineMailFilter, DoesntContain, Keywords, true); + testValidityTable(offlineMailFilter, Is, Keywords, true); + testValidityTable(offlineMailFilter, Isnt, Keywords, true); + testValidityTable(offlineMailFilter, IsEmpty, Keywords, true); + testValidityTable(offlineMailFilter, IsntEmpty, Keywords, true); + testValidityTable(offlineMailFilter, IsBefore, Keywords, false); + + // online mail + testValidityTable(onlineMail, Contains, Keywords, true); + testValidityTable(onlineMail, DoesntContain, Keywords, true); + testValidityTable(onlineMail, Is, Keywords, false); + testValidityTable(onlineMail, Isnt, Keywords, false); + testValidityTable(onlineMail, IsEmpty, Keywords, false); + testValidityTable(onlineMail, IsntEmpty, Keywords, false); + testValidityTable(onlineMail, IsBefore, Keywords, false); + + // online mail filter + testValidityTable(onlineMailFilter, Contains, Keywords, true); + testValidityTable(onlineMailFilter, DoesntContain, Keywords, true); + testValidityTable(onlineMailFilter, Is, Keywords, true); + testValidityTable(onlineMailFilter, Isnt, Keywords, true); + testValidityTable(onlineMailFilter, IsEmpty, Keywords, true); + testValidityTable(onlineMailFilter, IsntEmpty, Keywords, true); + testValidityTable(onlineMailFilter, IsBefore, Keywords, false); + + // news + testValidityTable(news, Contains, Keywords, false); + testValidityTable(news, DoesntContain, Keywords, false); + testValidityTable(news, Is, Keywords, false); + testValidityTable(news, Isnt, Keywords, false); + testValidityTable(news, IsEmpty, Keywords, false); + testValidityTable(news, IsntEmpty, Keywords, false); + testValidityTable(news, IsBefore, Keywords, false); + + // delete any existing tags + let tagArray = MailServices.tags.getAllTags(); + for (var i = 0; i < tagArray.length; i++) { + MailServices.tags.deleteKey(tagArray[i].key); + } + + // add as valid tags Tag1 and Tag4 + MailServices.tags.addTagForKey(Tag1, Tag1, null, null); + MailServices.tags.addTagForKey(Tag4, Tag4, null, null); + + var copyListener = { + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) { + hdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey); + }, + SetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + testKeywordSearch(); + }, + }; + + // Get a message into the local filestore. function testKeywordSearch() continues the testing after the copy. + var bugmail1 = do_get_file("../../../data/bugmail1"); + do_test_pending(); + MailServices.copy.copyFileMessage( + bugmail1, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + copyListener, + null + ); +} + +// process each test from queue, calls itself upon completion of each search +function testKeywordSearch() { + var test = Tests.shift(); + if (test) { + hdr.setStringProperty("keywords", test.msgTag); + new TestSearch( + localAccountUtils.inboxFolder, + test.testTag, + Ci.nsMsgSearchAttrib.Keywords, + test.op, + test.count, + testKeywordSearch + ); + } else { + hdr = null; + do_test_finished(); + } +} diff --git a/comm/mailnews/search/test/unit/test_searchUint32HdrProperty.js b/comm/mailnews/search/test/unit/test_searchUint32HdrProperty.js new file mode 100644 index 0000000000..e31f4db7d2 --- /dev/null +++ b/comm/mailnews/search/test/unit/test_searchUint32HdrProperty.js @@ -0,0 +1,141 @@ +/* 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/. */ + +/* + * Testing of Uint32HdrProperty search attribute. Adapted from test_search.js + */ + +/* import-globals-from ../../../test/resources/searchTestUtils.js */ +load("../../../resources/searchTestUtils.js"); + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var Isnt = Ci.nsMsgSearchOp.Isnt; +var Is = Ci.nsMsgSearchOp.Is; +var IsGreaterThan = Ci.nsMsgSearchOp.IsGreaterThan; +var IsLessThan = Ci.nsMsgSearchOp.IsLessThan; + +var Tests = [ + // test a property that does not exist + { + hdrProperty: "idonotexist", + op: Is, + value: 1, + count: 0, + }, + { + hdrProperty: "idonotexist", + op: Isnt, + value: 1, + count: 1, + }, + // add a property and test its value + { + setup: function setupProperty() { + for (let msgHdr of localAccountUtils.inboxFolder.msgDatabase.enumerateMessages()) { + msgHdr.setUint32Property("iam23", 23); + } + }, + hdrProperty: "iam23", + op: Is, + value: 23, + count: 1, + }, + { + hdrProperty: "iam23", + op: Isnt, + value: 23, + count: 0, + }, + { + hdrProperty: "iam23", + op: Is, + value: 17, + count: 0, + }, + { + hdrProperty: "iam23", + op: Isnt, + value: 17, + count: 1, + }, + { + hdrProperty: "iam23", + op: IsGreaterThan, + value: 25, + count: 0, + }, + { + hdrProperty: "iam23", + op: IsLessThan, + value: 25, + count: 1, + }, + { + hdrProperty: "iam23", + op: IsGreaterThan, + value: 17, + count: 1, + }, + { + hdrProperty: "iam23", + op: IsLessThan, + value: 17, + count: 0, + }, +]; + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + + var copyListener = { + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) {}, + SetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + testSearch(); + }, + }; + + // Get a message into the local filestore. function testSearch() continues + // the testing after the copy. + var bugmail1 = do_get_file("../../../data/bugmail1"); + do_test_pending(); + MailServices.copy.copyFileMessage( + bugmail1, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + copyListener, + null + ); +} + +// process each test from queue, calls itself upon completion of each search +function testSearch() { + var test = Tests.shift(); + if (test) { + if (test.setup) { + test.setup(); + } + new TestSearch( + localAccountUtils.inboxFolder, + test.value, + Ci.nsMsgSearchAttrib.Uint32HdrProperty, + test.op, + test.count, + testSearch, + null, + null, + test.hdrProperty + ); + } else { + do_test_finished(); + } +} diff --git a/comm/mailnews/search/test/unit/xpcshell.ini b/comm/mailnews/search/test/unit/xpcshell.ini new file mode 100644 index 0000000000..3157b90aea --- /dev/null +++ b/comm/mailnews/search/test/unit/xpcshell.ini @@ -0,0 +1,20 @@ +[DEFAULT] +head = head_mailbase.js +tail = + +[test_base64_decoding.js] +[test_bug366491.js] +[test_bug404489.js] +[test_copyThenMoveManual.js] +[test_junkWhitelisting.js] +[test_quarantineFilterMove.js] +[test_search.js] +[test_searchAddressInAb.js] +[test_searchBody.js] +[test_searchBoolean.js] +[test_searchChaining.js] +[test_searchCustomTerm.js] +[test_searchJunk.js] +[test_searchLocalizationStrings.js] +[test_searchTag.js] +[test_searchUint32HdrProperty.js] |