From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- comm/mailnews/search/content/CustomHeaders.js | 194 +++ comm/mailnews/search/content/CustomHeaders.xhtml | 61 + comm/mailnews/search/content/FilterEditor.js | 809 +++++++++ comm/mailnews/search/content/FilterEditor.xhtml | 136 ++ comm/mailnews/search/content/searchTerm.inc.xhtml | 27 + comm/mailnews/search/content/searchTerm.js | 568 +++++++ comm/mailnews/search/content/searchWidgets.js | 1779 +++++++++++++++++++ comm/mailnews/search/content/viewLog.js | 38 + comm/mailnews/search/content/viewLog.xhtml | 65 + comm/mailnews/search/public/moz.build | 37 + comm/mailnews/search/public/nsIMsgFilter.idl | 124 ++ .../search/public/nsIMsgFilterCustomAction.idl | 88 + .../search/public/nsIMsgFilterHitNotify.idl | 26 + comm/mailnews/search/public/nsIMsgFilterList.idl | 115 ++ comm/mailnews/search/public/nsIMsgFilterPlugin.idl | 331 ++++ .../mailnews/search/public/nsIMsgFilterService.idl | 102 ++ .../search/public/nsIMsgOperationListener.idl | 17 + .../mailnews/search/public/nsIMsgSearchAdapter.idl | 41 + .../search/public/nsIMsgSearchCustomTerm.idl | 75 + comm/mailnews/search/public/nsIMsgSearchNotify.idl | 30 + .../search/public/nsIMsgSearchScopeTerm.idl | 19 + .../mailnews/search/public/nsIMsgSearchSession.idl | 130 ++ comm/mailnews/search/public/nsIMsgSearchTerm.idl | 153 ++ .../search/public/nsIMsgSearchValidityManager.idl | 26 + .../search/public/nsIMsgSearchValidityTable.idl | 32 + comm/mailnews/search/public/nsIMsgSearchValue.idl | 35 + comm/mailnews/search/public/nsIMsgTraitService.idl | 182 ++ comm/mailnews/search/public/nsMsgBodyHandler.h | 110 ++ comm/mailnews/search/public/nsMsgFilterCore.idl | 62 + comm/mailnews/search/public/nsMsgResultElement.h | 40 + comm/mailnews/search/public/nsMsgSearchAdapter.h | 242 +++ .../search/public/nsMsgSearchBoolExpression.h | 110 ++ comm/mailnews/search/public/nsMsgSearchCore.idl | 206 +++ comm/mailnews/search/public/nsMsgSearchScopeTerm.h | 44 + comm/mailnews/search/public/nsMsgSearchTerm.h | 89 + comm/mailnews/search/src/Bogofilter.sfd | 14 + comm/mailnews/search/src/DSPAM.sfd | 14 + comm/mailnews/search/src/Habeas.sfd | 8 + comm/mailnews/search/src/MsgTraitService.jsm | 199 +++ comm/mailnews/search/src/POPFile.sfd | 14 + comm/mailnews/search/src/PeriodicFilterManager.jsm | 202 +++ comm/mailnews/search/src/SpamAssassin.sfd | 14 + comm/mailnews/search/src/SpamCatcher.sfd | 14 + comm/mailnews/search/src/SpamPal.sfd | 14 + comm/mailnews/search/src/components.conf | 40 + comm/mailnews/search/src/moz.build | 37 + comm/mailnews/search/src/nsMsgBodyHandler.cpp | 464 +++++ comm/mailnews/search/src/nsMsgFilter.cpp | 864 ++++++++++ comm/mailnews/search/src/nsMsgFilter.h | 100 ++ comm/mailnews/search/src/nsMsgFilterList.cpp | 1207 +++++++++++++ comm/mailnews/search/src/nsMsgFilterList.h | 74 + comm/mailnews/search/src/nsMsgFilterService.cpp | 1374 +++++++++++++++ comm/mailnews/search/src/nsMsgFilterService.h | 44 + comm/mailnews/search/src/nsMsgImapSearch.cpp | 991 +++++++++++ comm/mailnews/search/src/nsMsgLocalSearch.cpp | 919 ++++++++++ comm/mailnews/search/src/nsMsgLocalSearch.h | 90 + comm/mailnews/search/src/nsMsgSearchAdapter.cpp | 1109 ++++++++++++ comm/mailnews/search/src/nsMsgSearchImap.h | 34 + comm/mailnews/search/src/nsMsgSearchNews.cpp | 452 +++++ comm/mailnews/search/src/nsMsgSearchNews.h | 54 + comm/mailnews/search/src/nsMsgSearchSession.cpp | 576 +++++++ comm/mailnews/search/src/nsMsgSearchSession.h | 87 + comm/mailnews/search/src/nsMsgSearchTerm.cpp | 1797 ++++++++++++++++++++ comm/mailnews/search/src/nsMsgSearchValue.cpp | 96 ++ comm/mailnews/search/src/nsMsgSearchValue.h | 25 + comm/mailnews/search/test/moz.build | 6 + comm/mailnews/search/test/unit/head_mailbase.js | 23 + .../search/test/unit/test_base64_decoding.js | 94 + comm/mailnews/search/test/unit/test_bug366491.js | 110 ++ comm/mailnews/search/test/unit/test_bug404489.js | 202 +++ .../search/test/unit/test_copyThenMoveManual.js | 116 ++ .../search/test/unit/test_junkWhitelisting.js | 204 +++ .../search/test/unit/test_quarantineFilterMove.js | 181 ++ comm/mailnews/search/test/unit/test_search.js | 623 +++++++ .../search/test/unit/test_searchAddressInAb.js | 337 ++++ comm/mailnews/search/test/unit/test_searchBody.js | 294 ++++ .../search/test/unit/test_searchBoolean.js | 239 +++ .../search/test/unit/test_searchChaining.js | 90 + .../search/test/unit/test_searchCustomTerm.js | 112 ++ comm/mailnews/search/test/unit/test_searchJunk.js | 322 ++++ .../test/unit/test_searchLocalizationStrings.js | 61 + comm/mailnews/search/test/unit/test_searchTag.js | 490 ++++++ .../test/unit/test_searchUint32HdrProperty.js | 141 ++ comm/mailnews/search/test/unit/xpcshell.ini | 20 + 84 files changed, 20735 insertions(+) create mode 100644 comm/mailnews/search/content/CustomHeaders.js create mode 100644 comm/mailnews/search/content/CustomHeaders.xhtml create mode 100644 comm/mailnews/search/content/FilterEditor.js create mode 100644 comm/mailnews/search/content/FilterEditor.xhtml create mode 100644 comm/mailnews/search/content/searchTerm.inc.xhtml create mode 100644 comm/mailnews/search/content/searchTerm.js create mode 100644 comm/mailnews/search/content/searchWidgets.js create mode 100644 comm/mailnews/search/content/viewLog.js create mode 100644 comm/mailnews/search/content/viewLog.xhtml create mode 100644 comm/mailnews/search/public/moz.build create mode 100644 comm/mailnews/search/public/nsIMsgFilter.idl create mode 100644 comm/mailnews/search/public/nsIMsgFilterCustomAction.idl create mode 100644 comm/mailnews/search/public/nsIMsgFilterHitNotify.idl create mode 100644 comm/mailnews/search/public/nsIMsgFilterList.idl create mode 100644 comm/mailnews/search/public/nsIMsgFilterPlugin.idl create mode 100644 comm/mailnews/search/public/nsIMsgFilterService.idl create mode 100644 comm/mailnews/search/public/nsIMsgOperationListener.idl create mode 100644 comm/mailnews/search/public/nsIMsgSearchAdapter.idl create mode 100644 comm/mailnews/search/public/nsIMsgSearchCustomTerm.idl create mode 100644 comm/mailnews/search/public/nsIMsgSearchNotify.idl create mode 100644 comm/mailnews/search/public/nsIMsgSearchScopeTerm.idl create mode 100644 comm/mailnews/search/public/nsIMsgSearchSession.idl create mode 100644 comm/mailnews/search/public/nsIMsgSearchTerm.idl create mode 100644 comm/mailnews/search/public/nsIMsgSearchValidityManager.idl create mode 100644 comm/mailnews/search/public/nsIMsgSearchValidityTable.idl create mode 100644 comm/mailnews/search/public/nsIMsgSearchValue.idl create mode 100644 comm/mailnews/search/public/nsIMsgTraitService.idl create mode 100644 comm/mailnews/search/public/nsMsgBodyHandler.h create mode 100644 comm/mailnews/search/public/nsMsgFilterCore.idl create mode 100644 comm/mailnews/search/public/nsMsgResultElement.h create mode 100644 comm/mailnews/search/public/nsMsgSearchAdapter.h create mode 100644 comm/mailnews/search/public/nsMsgSearchBoolExpression.h create mode 100644 comm/mailnews/search/public/nsMsgSearchCore.idl create mode 100644 comm/mailnews/search/public/nsMsgSearchScopeTerm.h create mode 100644 comm/mailnews/search/public/nsMsgSearchTerm.h create mode 100644 comm/mailnews/search/src/Bogofilter.sfd create mode 100644 comm/mailnews/search/src/DSPAM.sfd create mode 100644 comm/mailnews/search/src/Habeas.sfd create mode 100644 comm/mailnews/search/src/MsgTraitService.jsm create mode 100644 comm/mailnews/search/src/POPFile.sfd create mode 100644 comm/mailnews/search/src/PeriodicFilterManager.jsm create mode 100644 comm/mailnews/search/src/SpamAssassin.sfd create mode 100644 comm/mailnews/search/src/SpamCatcher.sfd create mode 100644 comm/mailnews/search/src/SpamPal.sfd create mode 100644 comm/mailnews/search/src/components.conf create mode 100644 comm/mailnews/search/src/moz.build create mode 100644 comm/mailnews/search/src/nsMsgBodyHandler.cpp create mode 100644 comm/mailnews/search/src/nsMsgFilter.cpp create mode 100644 comm/mailnews/search/src/nsMsgFilter.h create mode 100644 comm/mailnews/search/src/nsMsgFilterList.cpp create mode 100644 comm/mailnews/search/src/nsMsgFilterList.h create mode 100644 comm/mailnews/search/src/nsMsgFilterService.cpp create mode 100644 comm/mailnews/search/src/nsMsgFilterService.h create mode 100644 comm/mailnews/search/src/nsMsgImapSearch.cpp create mode 100644 comm/mailnews/search/src/nsMsgLocalSearch.cpp create mode 100644 comm/mailnews/search/src/nsMsgLocalSearch.h create mode 100644 comm/mailnews/search/src/nsMsgSearchAdapter.cpp create mode 100644 comm/mailnews/search/src/nsMsgSearchImap.h create mode 100644 comm/mailnews/search/src/nsMsgSearchNews.cpp create mode 100644 comm/mailnews/search/src/nsMsgSearchNews.h create mode 100644 comm/mailnews/search/src/nsMsgSearchSession.cpp create mode 100644 comm/mailnews/search/src/nsMsgSearchSession.h create mode 100644 comm/mailnews/search/src/nsMsgSearchTerm.cpp create mode 100644 comm/mailnews/search/src/nsMsgSearchValue.cpp create mode 100644 comm/mailnews/search/src/nsMsgSearchValue.h create mode 100644 comm/mailnews/search/test/moz.build create mode 100644 comm/mailnews/search/test/unit/head_mailbase.js create mode 100644 comm/mailnews/search/test/unit/test_base64_decoding.js create mode 100644 comm/mailnews/search/test/unit/test_bug366491.js create mode 100644 comm/mailnews/search/test/unit/test_bug404489.js create mode 100644 comm/mailnews/search/test/unit/test_copyThenMoveManual.js create mode 100644 comm/mailnews/search/test/unit/test_junkWhitelisting.js create mode 100644 comm/mailnews/search/test/unit/test_quarantineFilterMove.js create mode 100644 comm/mailnews/search/test/unit/test_search.js create mode 100644 comm/mailnews/search/test/unit/test_searchAddressInAb.js create mode 100644 comm/mailnews/search/test/unit/test_searchBody.js create mode 100644 comm/mailnews/search/test/unit/test_searchBoolean.js create mode 100644 comm/mailnews/search/test/unit/test_searchChaining.js create mode 100644 comm/mailnews/search/test/unit/test_searchCustomTerm.js create mode 100644 comm/mailnews/search/test/unit/test_searchJunk.js create mode 100644 comm/mailnews/search/test/unit/test_searchLocalizationStrings.js create mode 100644 comm/mailnews/search/test/unit/test_searchTag.js create mode 100644 comm/mailnews/search/test/unit/test_searchUint32HdrProperty.js create mode 100644 comm/mailnews/search/test/unit/xpcshell.ini (limited to 'comm/mailnews/search') 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 @@ + + +#ifdef MOZ_THUNDERBIRD + +#else + +#endif + + + + + + &window.title; + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + %filterEditorDTD; + + %searchTermDTD; +]> + + + &window.title; + + + + + + + + + + + + + + + + + + + + + + + + + &contextDesc.label; + + + + + + + + + + + + + + + + + + + + + + + + + + + +#include searchTerm.inc.xhtml + + + + + + + + + + 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/. + + + + + + + + + + + + + + + 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 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 + 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( + ` + + + + + + + + + + `, + ["chrome://messenger/locale/FilterEditor.dtd"] + ) + ); + + updateParentNode(this.closest(".ruleaction")); + } + } + + class MozRuleactiontargetJunkscore extends MozXULElement { + connectedCallback() { + this.appendChild( + MozXULElement.parseXULToFragment( + ` + + + + + + + `, + ["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( + ` + + + + + `, + ["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( + ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `, + [ + "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 @@ + + + + + + + + + + &viewLog.title; + + + + + + &viewLogInfo.text; + + + + + + 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 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 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 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 aTraits, + in Array 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 tokenStrings, + in Array tokenPercents, + in Array 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 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 aProTraits, + in Array 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 aMsgURIs, + in Array aProTraits, + in Array 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 aOldTraits, + in Array 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 aFromTraits, + [optional] in Array 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 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 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 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 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 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 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 getAvailableAttributes(); + Array 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 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 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 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 m_fileLineStream; + nsCOMPtr 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 m_msgHdr; + nsCOMPtr 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 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 > 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> const&); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGSEARCHADAPTER + + nsIMsgSearchScopeTerm* m_scope; + nsTArray> + m_searchTerms; /* linked list of criteria terms */ + + nsString m_defaultCharset = u"UTF-8"_ns; + + static nsresult EncodeImap( + char** ppEncoding, nsTArray> 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 m_offlineMailTable; + nsCOMPtr m_offlineMailFilterTable; + nsCOMPtr m_onlineMailTable; + nsCOMPtr m_onlineMailFilterTable; + nsCOMPtr m_onlineManualFilterTable; + + nsCOMPtr m_newsTable; // online news + + // Local news tables, used for local news searching or offline. + nsCOMPtr m_localNewsTable; // base table + nsCOMPtr m_localNewsJunkTable; // base + junk + nsCOMPtr m_localNewsBodyTable; // base + body + nsCOMPtr + m_localNewsJunkBodyTable; // base + junk + body + nsCOMPtr m_ldapTable; + nsCOMPtr m_ldapAndTable; + nsCOMPtr m_localABTable; + nsCOMPtr m_localABAndTable; + nsCOMPtr 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> const& termList); + + char* GetStatusBarName(); + + nsMsgSearchScopeValue m_attribute; + nsCOMPtr m_folder; + nsCOMPtr m_adapter; + nsCOMPtr 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 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 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 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>& 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>& terms) { + delete m_expressionTree; + m_expressionTree = nullptr; + terms = m_termList.Clone(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilter::SetSearchTerms( + nsTArray> 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 bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + + nsCOMPtr 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 - 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 logErrorFormatStrings = {tErrmsg, tcode16}; + + nsString filterFailureWarningPrefix; + rv = bundle->FormatStringFromName("filterFailureWarningPrefix", + logErrorFormatStrings, + filterFailureWarningPrefix); + NS_ENSURE_SUCCESS(rv, rv); + buffer += filterFailureWarningPrefix; + buffer.AppendLiteral("\n"); + } + + AutoTArray 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 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 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 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 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 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 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 accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr 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 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 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 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 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> m_termList; /* criteria terms */ + nsCOMPtr + m_scope; /* default for mail rules is inbox, but news rules could + have a newsgroup - LDAP would be invalid */ + nsTArray> 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 + +// 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 \ + "\n\n\n\n\n\n\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 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 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 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 thisFolder; + rv = m_folder->GetFilePath(getter_AddRefs(thisFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr 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 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 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 filter; + uint32_t filterCount = 0; + nsresult rv = GetFilterCount(&filterCount); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr scope = + new nsMsgSearchScopeTerm(nullptr, nsMsgSearchScope::offlineMail, folder); + + nsString folderName; + folder->GetName(folderName); + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + nsCString typeName; + nsCOMPtr 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(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 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 aStream) { + nsresult err = NS_OK; + uint64_t bytesAvailable; + + nsCOMPtr bufStream; + nsCOMPtr stream = std::move(aStream); + err = NS_NewBufferedInputStream(getter_AddRefs(bufStream), stream.forget(), + FILE_IO_BUFFER_SIZE); + NS_ENSURE_SUCCESS(err, err); + + nsMsgFilterFileAttribValue attrib; + nsCOMPtr 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(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 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 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 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 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 tempFilter1; + rv = GetFilterAt(newIndex, getter_AddRefs(tempFilter1)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr 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 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 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 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> 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 "

\n" +#define LOG_ENTRY_START_TAG_LEN (strlen(LOG_ENTRY_START_TAG)) +#define LOG_ENTRY_END_TAG "

\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 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 bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + + nsCOMPtr 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 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