diff options
Diffstat (limited to 'comm/mailnews/search/src/nsMsgFilterService.cpp')
-rw-r--r-- | comm/mailnews/search/src/nsMsgFilterService.cpp | 1374 |
1 files changed, 1374 insertions, 0 deletions
diff --git a/comm/mailnews/search/src/nsMsgFilterService.cpp b/comm/mailnews/search/src/nsMsgFilterService.cpp new file mode 100644 index 0000000000..1adfe7cee9 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgFilterService.cpp @@ -0,0 +1,1374 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// this file implements the nsMsgFilterService interface + +#include "msgCore.h" +#include "nsMsgFilterService.h" +#include "nsMsgFilterList.h" +#include "nsMsgSearchScopeTerm.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIPrompt.h" +#include "nsIDocShell.h" +#include "nsIStringBundle.h" +#include "nsIMsgSearchNotify.h" +#include "nsIUrlListener.h" +#include "nsIMsgCopyServiceListener.h" +#include "nsIMsgLocalMailFolder.h" +#include "nsIMsgDatabase.h" +#include "nsIMsgHdr.h" +#include "nsIMsgCopyService.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsISafeOutputStream.h" +#include "nsIMsgComposeService.h" +#include "nsNetUtil.h" +#include "nsMsgUtils.h" +#include "nsIMsgMailSession.h" +#include "nsIFile.h" +#include "nsIMsgFilterCustomAction.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgWindow.h" +#include "nsIMsgSearchCustomTerm.h" +#include "nsIMsgSearchTerm.h" +#include "nsIMsgThread.h" +#include "nsIMsgFilter.h" +#include "nsIMsgOperationListener.h" +#include "mozilla/Components.h" +#include "mozilla/Logging.h" + +using namespace mozilla; + +LazyLogModule FILTERLOGMODULE("Filters"); + +#define BREAK_IF_FAILURE(_rv, _text) \ + if (NS_FAILED(_rv)) { \ + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, \ + ("(Post) Filter error: %s", _text)); \ + m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \ + m_curFilter); \ + NS_WARNING(_text); \ + mFinalResult = _rv; \ + break; \ + } + +#define CONTINUE_IF_FAILURE(_rv, _text) \ + if (NS_FAILED(_rv)) { \ + MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning, \ + ("(Post) Filter problem: %s", _text)); \ + m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \ + m_curFilter); \ + NS_WARNING(_text); \ + mFinalResult = _rv; \ + if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); \ + continue; \ + } + +#define BREAK_IF_FALSE(_assertTrue, _text) \ + if (MOZ_UNLIKELY(!(_assertTrue))) { \ + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, \ + ("(Post) Filter error: %s", _text)); \ + m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \ + m_curFilter); \ + NS_WARNING(_text); \ + mFinalResult = NS_ERROR_FAILURE; \ + break; \ + } + +#define CONTINUE_IF_FALSE(_assertTrue, _text) \ + if (MOZ_UNLIKELY(!(_assertTrue))) { \ + MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning, \ + ("(Post) Filter problem: %s", _text)); \ + m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \ + m_curFilter); \ + NS_WARNING(_text); \ + mFinalResult = NS_ERROR_FAILURE; \ + if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); \ + continue; \ + } + +#define BREAK_ACTION(_text) \ + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, \ + ("(Post) Filter Error: %s", _text)); \ + if (loggingEnabled) \ + m_filters->LogFilterMessage(NS_LITERAL_STRING_FROM_CSTRING(_text), \ + m_curFilter); \ + NS_WARNING(_text); \ + if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); \ + break; + +#define BREAK_ACTION_IF_FALSE(_assertTrue, _text) \ + if (MOZ_UNLIKELY(!(_assertTrue))) { \ + finalResult = NS_ERROR_FAILURE; \ + BREAK_ACTION(_text); \ + } + +#define BREAK_ACTION_IF_FAILURE(_rv, _text) \ + if (NS_FAILED(_rv)) { \ + finalResult = _rv; \ + BREAK_ACTION(_text); \ + } + +NS_IMPL_ISUPPORTS(nsMsgFilterService, nsIMsgFilterService) + +nsMsgFilterService::nsMsgFilterService() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, ("nsMsgFilterService")); +} + +nsMsgFilterService::~nsMsgFilterService() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, ("~nsMsgFilterService")); +} + +NS_IMETHODIMP nsMsgFilterService::OpenFilterList( + nsIFile* aFilterFile, nsIMsgFolder* rootFolder, nsIMsgWindow* aMsgWindow, + nsIMsgFilterList** resultFilterList) { + NS_ENSURE_ARG_POINTER(aFilterFile); + NS_ENSURE_ARG_POINTER(resultFilterList); + + nsresult rv; + if (rootFolder) { + nsCOMPtr<nsIMsgIncomingServer> server; + rv = rootFolder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + nsString serverName; + server->GetPrettyName(serverName); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Reading filter list for account '%s'", + NS_ConvertUTF16toUTF8(serverName).get())); + } + + nsString fileName; + (void)aFilterFile->GetPath(fileName); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("Reading filter list from file '%s'", + NS_ConvertUTF16toUTF8(fileName).get())); + + bool exists = false; + rv = aFilterFile->Exists(&exists); + if (NS_FAILED(rv) || !exists) { + rv = aFilterFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIInputStream> fileStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(fileStream), aFilterFile); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(fileStream, NS_ERROR_OUT_OF_MEMORY); + + RefPtr<nsMsgFilterList> filterList = new nsMsgFilterList(); + filterList->SetFolder(rootFolder); + + // temporarily tell the filter where its file path is + filterList->SetDefaultFile(aFilterFile); + + int64_t size = 0; + rv = aFilterFile->GetFileSize(&size); + if (NS_SUCCEEDED(rv) && size > 0) + rv = filterList->LoadTextFilters(fileStream.forget()); + if (NS_SUCCEEDED(rv)) { + int16_t version; + filterList->GetVersion(&version); + if (version != kFileVersion) SaveFilterList(filterList, aFilterFile); + } else { + if (rv == NS_MSG_FILTER_PARSE_ERROR && aMsgWindow) { + rv = BackUpFilterFile(aFilterFile, aMsgWindow); + NS_ENSURE_SUCCESS(rv, rv); + rv = aFilterFile->SetFileSize(0); + NS_ENSURE_SUCCESS(rv, rv); + return OpenFilterList(aFilterFile, rootFolder, aMsgWindow, + resultFilterList); + } else if (rv == NS_MSG_CUSTOM_HEADERS_OVERFLOW && aMsgWindow) + ThrowAlertMsg("filterCustomHeaderOverflow", aMsgWindow); + else if (rv == NS_MSG_INVALID_CUSTOM_HEADER && aMsgWindow) + ThrowAlertMsg("invalidCustomHeader", aMsgWindow); + } + + nsCString listId; + filterList->GetListId(listId); + uint32_t filterCount = 0; + (void)filterList->GetFilterCount(&filterCount); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Read %" PRIu32 " filters", filterCount)); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Filter list stored as %s", listId.get())); + + filterList.forget(resultFilterList); + return rv; +} + +NS_IMETHODIMP nsMsgFilterService::CloseFilterList( + nsIMsgFilterList* filterList) { + // NS_ASSERTION(false,"CloseFilterList doesn't do anything yet"); + return NS_OK; +} + +/* save without deleting */ +NS_IMETHODIMP nsMsgFilterService::SaveFilterList(nsIMsgFilterList* filterList, + nsIFile* filterFile) { + NS_ENSURE_ARG_POINTER(filterFile); + NS_ENSURE_ARG_POINTER(filterList); + + nsCString listId; + filterList->GetListId(listId); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Saving filter list %s", listId.get())); + + nsCOMPtr<nsIOutputStream> strm; + nsresult rv = MsgNewSafeBufferedFileOutputStream(getter_AddRefs(strm), + filterFile, -1, 0600); + NS_ENSURE_SUCCESS(rv, rv); + + rv = filterList->SaveToFile(strm); + + nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(strm); + NS_ASSERTION(safeStream, "expected a safe output stream!"); + if (safeStream) { + rv = safeStream->Finish(); + if (NS_FAILED(rv)) { + NS_WARNING("failed to save filter file! possible data loss"); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, ("Save of list failed")); + } + } + return rv; +} + +NS_IMETHODIMP nsMsgFilterService::CancelFilterList( + nsIMsgFilterList* filterList) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsMsgFilterService::BackUpFilterFile(nsIFile* aFilterFile, + nsIMsgWindow* aMsgWindow) { + AlertBackingUpFilterFile(aMsgWindow); + + nsCOMPtr<nsIFile> localParentDir; + nsresult rv = aFilterFile->GetParent(getter_AddRefs(localParentDir)); + NS_ENSURE_SUCCESS(rv, rv); + + // if back-up file exists delete the back up file otherwise copy fails. + nsCOMPtr<nsIFile> backupFile; + rv = localParentDir->Clone(getter_AddRefs(backupFile)); + NS_ENSURE_SUCCESS(rv, rv); + backupFile->AppendNative("rulesbackup.dat"_ns); + bool exists; + backupFile->Exists(&exists); + if (exists) backupFile->Remove(false); + + return aFilterFile->CopyToNative(localParentDir, "rulesbackup.dat"_ns); +} + +nsresult nsMsgFilterService::AlertBackingUpFilterFile( + nsIMsgWindow* aMsgWindow) { + return ThrowAlertMsg("filterListBackUpMsg", aMsgWindow); +} + +// Do not use this routine if you have to call it very often because it creates +// a new bundle each time. +nsresult nsMsgFilterService::GetStringFromBundle(const char* aMsgName, + nsAString& aResult) { + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv = GetFilterStringBundle(getter_AddRefs(bundle)); + if (NS_SUCCEEDED(rv) && bundle) + rv = bundle->GetStringFromName(aMsgName, aResult); + return rv; +} + +nsresult nsMsgFilterService::GetFilterStringBundle(nsIStringBundle** aBundle) { + NS_ENSURE_ARG_POINTER(aBundle); + + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsIStringBundle> bundle; + if (bundleService) + bundleService->CreateBundle("chrome://messenger/locale/filter.properties", + getter_AddRefs(bundle)); + bundle.forget(aBundle); + return NS_OK; +} + +nsresult nsMsgFilterService::ThrowAlertMsg(const char* aMsgName, + nsIMsgWindow* aMsgWindow) { + nsString alertString; + nsresult rv = GetStringFromBundle(aMsgName, alertString); + nsCOMPtr<nsIMsgWindow> msgWindow = aMsgWindow; + if (!msgWindow) { + nsCOMPtr<nsIMsgMailSession> mailSession( + do_GetService("@mozilla.org/messenger/services/session;1", &rv)); + if (NS_SUCCEEDED(rv)) + rv = mailSession->GetTopmostMsgWindow(getter_AddRefs(msgWindow)); + } + + if (NS_SUCCEEDED(rv) && !alertString.IsEmpty() && msgWindow) { + nsCOMPtr<nsIDocShell> docShell; + msgWindow->GetRootDocShell(getter_AddRefs(docShell)); + if (docShell) { + nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell)); + if (dialog && !alertString.IsEmpty()) + dialog->Alert(nullptr, alertString.get()); + } + } + return rv; +} + +// this class is used to run filters after the fact, i.e., after new mail has +// been downloaded from the server. It can do the following: +// 1. Apply a single imap or pop3 filter on a single folder. +// 2. Apply multiple filters on a single imap or pop3 folder. +// 3. Apply a single filter on multiple imap or pop3 folders in the same +// account. +// 4. Apply multiple filters on multiple imap or pop3 folders in the same +// account. +// This will be called from the front end js code in the case of the +// apply filters to folder menu code, and from the filter dialog js code with +// the run filter now command. + +// this class holds the list of filters and folders, and applies them in turn, +// first iterating over all the filters on one folder, and then advancing to the +// next folder and repeating. For each filter,we take the filter criteria and +// create a search term list. Then, we execute the search. We are a search +// listener so that we can build up the list of search hits. Then, when the +// search is done, we will apply the filter action(s) en-masse, so, for example, +// if the action is a move, we calls one method to move all the messages to the +// destination folder. Or, mark all the messages read. In the case of imap +// operations, or imap/local moves, the action will be asynchronous, so we'll +// need to be a url listener as well, and kick off the next filter when the +// action completes. +class nsMsgFilterAfterTheFact : public nsIUrlListener, + public nsIMsgSearchNotify, + public nsIMsgCopyServiceListener { + public: + nsMsgFilterAfterTheFact(nsIMsgWindow* aMsgWindow, + nsIMsgFilterList* aFilterList, + const nsTArray<RefPtr<nsIMsgFolder>>& aFolderList, + nsIMsgOperationListener* aCallback); + NS_DECL_ISUPPORTS + NS_DECL_NSIURLLISTENER + NS_DECL_NSIMSGSEARCHNOTIFY + NS_DECL_NSIMSGCOPYSERVICELISTENER + + nsresult AdvanceToNextFolder(); // kicks off the process + protected: + virtual ~nsMsgFilterAfterTheFact(); + virtual nsresult RunNextFilter(); + /** + * apply filter actions to current search hits + */ + nsresult ApplyFilter(); + nsresult OnEndExecution(); // do what we have to do to cleanup. + bool ContinueExecutionPrompt(); + nsresult DisplayConfirmationPrompt(nsIMsgWindow* msgWindow, + const char16_t* confirmString, + bool* confirmed); + nsCOMPtr<nsIMsgWindow> m_msgWindow; + nsCOMPtr<nsIMsgFilterList> m_filters; + nsTArray<RefPtr<nsIMsgFolder>> m_folders; + nsCOMPtr<nsIMsgFolder> m_curFolder; + nsCOMPtr<nsIMsgDatabase> m_curFolderDB; + nsCOMPtr<nsIMsgFilter> m_curFilter; + uint32_t m_curFilterIndex; + uint32_t m_curFolderIndex; + uint32_t m_numFilters; + nsTArray<nsMsgKey> m_searchHits; + nsTArray<RefPtr<nsIMsgDBHdr>> m_searchHitHdrs; + nsTArray<nsMsgKey> m_stopFiltering; + nsCOMPtr<nsIMsgSearchSession> m_searchSession; + nsCOMPtr<nsIMsgOperationListener> m_callback; + uint32_t m_nextAction; // next filter action to perform + nsresult mFinalResult; // report of overall success or failure + bool mNeedsRelease; // Did we need to release ourself? +}; + +NS_IMPL_ISUPPORTS(nsMsgFilterAfterTheFact, nsIUrlListener, nsIMsgSearchNotify, + nsIMsgCopyServiceListener) + +nsMsgFilterAfterTheFact::nsMsgFilterAfterTheFact( + nsIMsgWindow* aMsgWindow, nsIMsgFilterList* aFilterList, + const nsTArray<RefPtr<nsIMsgFolder>>& aFolderList, + nsIMsgOperationListener* aCallback) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, ("(Post) nsMsgFilterAfterTheFact")); + m_curFilterIndex = m_curFolderIndex = m_nextAction = 0; + m_msgWindow = aMsgWindow; + m_filters = aFilterList; + m_folders = aFolderList.Clone(); + m_filters->GetFilterCount(&m_numFilters); + + NS_ADDREF_THIS(); // we own ourselves, and will release ourselves when + // execution is done. + mNeedsRelease = true; + + m_callback = aCallback; + mFinalResult = NS_OK; +} + +nsMsgFilterAfterTheFact::~nsMsgFilterAfterTheFact() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) ~nsMsgFilterAfterTheFact")); +} + +// do what we have to do to cleanup. +nsresult nsMsgFilterAfterTheFact::OnEndExecution() { + if (m_searchSession) m_searchSession->UnregisterListener(this); + + if (m_filters) (void)m_filters->FlushLogIfNecessary(); + + if (m_callback) (void)m_callback->OnStopOperation(mFinalResult); + + nsresult rv = mFinalResult; + // OnEndExecution() can be called a second time when a rule execution fails + // and the user is prompted whether he wants to continue. + if (mNeedsRelease) { + NS_RELEASE_THIS(); // release ourselves. + mNeedsRelease = false; + } + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, ("(Post) End executing filters")); + return rv; +} + +nsresult nsMsgFilterAfterTheFact::RunNextFilter() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgFilterAfterTheFact::RunNextFilter")); + nsresult rv = NS_OK; + while (true) { + m_curFilter = nullptr; + if (m_curFilterIndex >= m_numFilters) break; + + BREAK_IF_FALSE(m_filters, "Missing filters"); + + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Running filter %" PRIu32, m_curFilterIndex)); + + rv = + m_filters->GetFilterAt(m_curFilterIndex++, getter_AddRefs(m_curFilter)); + CONTINUE_IF_FAILURE(rv, "Could not get filter at index"); + + nsString filterName; + m_curFilter->GetFilterName(filterName); + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Filter name: %s", NS_ConvertUTF16toUTF8(filterName).get())); + // clang-format on + + nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms; + rv = m_curFilter->GetSearchTerms(searchTerms); + CONTINUE_IF_FAILURE(rv, "Could not get searchTerms"); + + if (m_searchSession) m_searchSession->UnregisterListener(this); + m_searchSession = + do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv); + BREAK_IF_FAILURE(rv, "Failed to get search session"); + + nsMsgSearchScopeValue searchScope = nsMsgSearchScope::offlineMail; + for (nsIMsgSearchTerm* term : searchTerms) { + rv = m_searchSession->AppendTerm(term); + BREAK_IF_FAILURE(rv, "Could not append search term"); + } + CONTINUE_IF_FAILURE(rv, "Failed to setup search terms"); + m_searchSession->RegisterListener(this, + nsIMsgSearchSession::allNotifications); + + rv = m_searchSession->AddScopeTerm(searchScope, m_curFolder); + CONTINUE_IF_FAILURE(rv, "Failed to add scope term"); + m_nextAction = 0; + rv = m_searchSession->Search(m_msgWindow); + CONTINUE_IF_FAILURE(rv, "Search failed"); + return NS_OK; // OnSearchDone will continue + } + + if (NS_FAILED(rv)) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, + ("(Post) Filter evaluation failed")); + m_filters->LogFilterMessage(u"Filter evaluation failed"_ns, m_curFilter); + } + + m_curFilter = nullptr; + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Search failed"); + return AdvanceToNextFolder(); +} + +nsresult nsMsgFilterAfterTheFact::AdvanceToNextFolder() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgFilterAfterTheFact::AdvanceToNextFolder")); + nsresult rv = NS_OK; + // Advance through folders, making sure m_curFolder is null on errors + while (true) { + m_stopFiltering.Clear(); + m_curFolder = nullptr; + if (m_curFolderIndex >= m_folders.Length()) { + // final end of nsMsgFilterAfterTheFact object + return OnEndExecution(); + } + + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Entering folder %" PRIu32, m_curFolderIndex)); + + // reset the filter index to apply all filters to this new folder + m_curFilterIndex = 0; + m_nextAction = 0; + m_curFolder = m_folders[m_curFolderIndex++]; + + // Note: I got rv = NS_OK but null m_curFolder after deleting a folder + // outside of TB, when I select a single message and "run filter on message" + // and the filter is to move the message to the deleted folder. + + // m_curFolder may be null when the folder is deleted externally. + CONTINUE_IF_FALSE(m_curFolder, "Next folder returned null"); + + nsString folderName; + (void)m_curFolder->GetName(folderName); + MOZ_LOG( + FILTERLOGMODULE, LogLevel::Info, + ("(Post) Folder name: %s", NS_ConvertUTF16toUTF8(folderName).get())); + + nsCOMPtr<nsIFile> folderPath; + (void)m_curFolder->GetFilePath(getter_AddRefs(folderPath)); + (void)folderPath->GetPath(folderName); + MOZ_LOG( + FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Folder path: %s", NS_ConvertUTF16toUTF8(folderName).get())); + + rv = m_curFolder->GetMsgDatabase(getter_AddRefs(m_curFolderDB)); + if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) { + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = + do_QueryInterface(m_curFolder, &rv); + if (NS_SUCCEEDED(rv) && localFolder) + // will continue with OnStopRunningUrl + return localFolder->ParseFolder(m_msgWindow, this); + } + CONTINUE_IF_FAILURE(rv, "Could not get folder db"); + + rv = RunNextFilter(); + // RunNextFilter returns success when either filters are done, or an async + // process has started. It will call AdvanceToNextFolder itself if possible, + // so no need to call here. + BREAK_IF_FAILURE(rv, "Failed to run next filter"); + break; + } + return rv; +} + +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnStartRunningUrl(nsIURI* aUrl) { + return NS_OK; +} + +// This is the return from a folder parse +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnStopRunningUrl(nsIURI* aUrl, + nsresult aExitCode) { + if (NS_SUCCEEDED(aExitCode)) return RunNextFilter(); + + mFinalResult = aExitCode; + // If m_msgWindow then we are in a context where the user can deal with + // errors. Put up a prompt, and exit if user wants. + if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); + + // folder parse failed, so stop processing this folder. + return AdvanceToNextFolder(); +} + +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnSearchHit(nsIMsgDBHdr* header, + nsIMsgFolder* folder) { + NS_ENSURE_ARG_POINTER(header); + + nsMsgKey msgKey; + header->GetMessageKey(&msgKey); + + nsCString msgId; + header->GetMessageId(getter_Copies(msgId)); + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Filter matched message with key %" PRIu32, + msgKeyToInt(msgKey))); + // clang-format on + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Matched message ID: %s", msgId.get())); + + // Under various previous actions (a move, delete, or stopExecution) + // we do not want to process filters on a per-message basis. + if (m_stopFiltering.Contains(msgKey)) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Stopping further filter execution on this message")); + return NS_OK; + } + + m_searchHits.AppendElement(msgKey); + m_searchHitHdrs.AppendElement(header); + return NS_OK; +} + +// Continue after an async operation. +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnSearchDone(nsresult status) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Done matching current filter")); + if (NS_SUCCEEDED(status)) + return m_searchHits.IsEmpty() ? RunNextFilter() : ApplyFilter(); + + mFinalResult = status; + if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); + + // The search failed, so move on to the next filter. + return RunNextFilter(); +} + +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnNewSearch() { + m_searchHits.Clear(); + m_searchHitHdrs.Clear(); + return NS_OK; +} + +// This method will apply filters. It will continue to advance though headers, +// filters, and folders until done, unless it starts an async operation with +// a callback. The callback should call ApplyFilter again. It only returns +// an error if it is impossible to continue after attempting to continue the +// next filter action, filter, or folder. +nsresult nsMsgFilterAfterTheFact::ApplyFilter() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgFilterAfterTheFact::ApplyFilter")); + nsresult rv; + do { + // Error management block, break if unable to continue with filter. + + if (!m_curFilter) + break; // Maybe not an error, we just need to call RunNextFilter(); + if (!m_curFolder) + break; // Maybe not an error, we just need to call AdvanceToNextFolder(); + + // 'm_curFolder' can be reset asynchronously by the copy service + // calling OnStopCopy(). So take a local copy here and use it throughout the + // function. + nsCOMPtr<nsIMsgFolder> curFolder = m_curFolder; + nsCOMPtr<nsIMsgFilter> curFilter = m_curFilter; + + // We're going to log the filter actions before firing them because some + // actions are async. + bool loggingEnabled = false; + if (m_filters) (void)m_filters->GetLoggingEnabled(&loggingEnabled); + + nsTArray<RefPtr<nsIMsgRuleAction>> actionList; + rv = curFilter->GetSortedActionList(actionList); + BREAK_IF_FAILURE(rv, "Could not get action list for filter"); + + uint32_t numActions = actionList.Length(); + + if (m_nextAction == 0) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Applying %" PRIu32 " filter actions to %" PRIu32 + " matched messages", + numActions, static_cast<uint32_t>(m_searchHits.Length()))); + } else if (m_nextAction < numActions) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Applying remaining %" PRIu32 + " filter actions to %" PRIu32 " matched messages", + numActions - m_nextAction, + static_cast<uint32_t>(m_searchHits.Length()))); + } + + // We start from m_nextAction to allow us to continue applying actions + // after the return from an async copy. + while (m_nextAction < numActions) { + nsresult finalResult = NS_OK; + nsCOMPtr<nsIMsgRuleAction> filterAction(actionList[m_nextAction]); + ++m_nextAction; + + nsMsgRuleActionType actionType; + rv = filterAction->GetType(&actionType); + CONTINUE_IF_FAILURE(rv, "Could not get type for filter action"); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Running filter action at index %" PRIu32 + ", action type = %i", + m_nextAction - 1, actionType)); + + nsCString actionTargetFolderUri; + if (actionType == nsMsgFilterAction::MoveToFolder || + actionType == nsMsgFilterAction::CopyToFolder) { + rv = filterAction->GetTargetFolderUri(actionTargetFolderUri); + CONTINUE_IF_FAILURE(rv, "GetTargetFolderUri failed"); + CONTINUE_IF_FALSE(!actionTargetFolderUri.IsEmpty(), + "actionTargetFolderUri is empty"); + } + + if (loggingEnabled) { + for (auto msgHdr : m_searchHitHdrs) { + (void)curFilter->LogRuleHit(filterAction, msgHdr); + } + } + + // all actions that pass "this" as a listener in order to chain filter + // execution when the action is finished need to return before reaching + // the bottom of this routine, because we run the next filter at the end + // of this routine. + switch (actionType) { + case nsMsgFilterAction::Delete: + // we can't pass ourselves in as a copy service listener because the + // copy service listener won't get called in several situations (e.g., + // the delete model is imap delete) and we rely on the listener + // getting called to continue the filter application. This means we're + // going to end up firing off the delete, and then subsequently + // issuing a search for the next filter, which will block until the + // delete finishes. + rv = curFolder->DeleteMessages(m_searchHitHdrs, m_msgWindow, false, + false, nullptr, false /*allow Undo*/); + BREAK_ACTION_IF_FAILURE(rv, "Deleting messages failed"); + + // don't allow any more filters on this message + m_stopFiltering.AppendElements(m_searchHits); + for (uint32_t i = 0; i < m_searchHits.Length(); i++) + curFolder->OrProcessingFlags(m_searchHits[i], + nsMsgProcessingFlags::FilterToMove); + // if we are deleting then we couldn't care less about applying + // remaining filter actions + m_nextAction = numActions; + break; + + case nsMsgFilterAction::MoveToFolder: + // Even if move fails we will not run additional actions, as they + // would not have run if move succeeded. + m_nextAction = numActions; + // Fall through to the copy case. + [[fallthrough]]; + case nsMsgFilterAction::CopyToFolder: { + nsCString uri; + curFolder->GetURI(uri); + + if (uri.Equals(actionTargetFolderUri)) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Target folder is the same as source folder, " + "skipping")); + break; + } + + nsCOMPtr<nsIMsgFolder> destIFolder; + rv = GetOrCreateFolder(actionTargetFolderUri, + getter_AddRefs(destIFolder)); + BREAK_ACTION_IF_FAILURE(rv, "Could not get action folder"); + + bool canFileMessages = true; + nsCOMPtr<nsIMsgFolder> parentFolder; + destIFolder->GetParent(getter_AddRefs(parentFolder)); + if (parentFolder) destIFolder->GetCanFileMessages(&canFileMessages); + if (!parentFolder || !canFileMessages) { + curFilter->SetEnabled(false); + destIFolder->ThrowAlertMsg("filterDisabled", m_msgWindow); + // we need to explicitly save the filter file. + m_filters->SaveToDefaultFile(); + // In the case of applying multiple filters + // we might want to remove the filter from the list, but + // that's a bit evil since we really don't know that we own + // the list. Disabling it doesn't do a lot of good since + // we still apply disabled filters. Currently, we don't + // have any clients that apply filters to multiple folders, + // so this might be the edge case of an edge case. + m_nextAction = numActions; + BREAK_ACTION_IF_FALSE(false, + "No parent folder or folder can't file " + "messages, disabling the filter"); + } + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv); + BREAK_ACTION_IF_FAILURE(rv, "Could not get copy service"); + + if (actionType == nsMsgFilterAction::MoveToFolder) { + m_stopFiltering.AppendElements(m_searchHits); + for (uint32_t i = 0; i < m_searchHits.Length(); i++) + curFolder->OrProcessingFlags(m_searchHits[i], + nsMsgProcessingFlags::FilterToMove); + } + + rv = copyService->CopyMessages( + curFolder, m_searchHitHdrs, destIFolder, + actionType == nsMsgFilterAction::MoveToFolder, this, m_msgWindow, + false); + BREAK_ACTION_IF_FAILURE(rv, "CopyMessages failed"); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Action execution continues async")); + return NS_OK; // OnStopCopy callback to continue; + } break; + case nsMsgFilterAction::MarkRead: + // crud, no listener support here - we'll probably just need to go on + // and apply the next filter, and, in the imap case, rely on multiple + // connection and url queueing to stay out of trouble + rv = curFolder->MarkMessagesRead(m_searchHitHdrs, true); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + break; + case nsMsgFilterAction::MarkUnread: + rv = curFolder->MarkMessagesRead(m_searchHitHdrs, false); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + break; + case nsMsgFilterAction::MarkFlagged: + rv = curFolder->MarkMessagesFlagged(m_searchHitHdrs, true); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + break; + case nsMsgFilterAction::KillThread: + case nsMsgFilterAction::WatchThread: { + for (auto msgHdr : m_searchHitHdrs) { + nsCOMPtr<nsIMsgThread> msgThread; + nsMsgKey threadKey; + m_curFolderDB->GetThreadContainingMsgHdr(msgHdr, + getter_AddRefs(msgThread)); + BREAK_ACTION_IF_FALSE(msgThread, "Could not find msg thread"); + msgThread->GetThreadKey(&threadKey); + if (actionType == nsMsgFilterAction::KillThread) { + rv = m_curFolderDB->MarkThreadIgnored(msgThread, threadKey, true, + nullptr); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + } else { + rv = m_curFolderDB->MarkThreadWatched(msgThread, threadKey, true, + nullptr); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + } + } + } break; + case nsMsgFilterAction::KillSubthread: { + for (auto msgHdr : m_searchHitHdrs) { + rv = m_curFolderDB->MarkHeaderKilled(msgHdr, true, nullptr); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + } + } break; + case nsMsgFilterAction::ChangePriority: { + nsMsgPriorityValue filterPriority; + filterAction->GetPriority(&filterPriority); + for (auto msgHdr : m_searchHitHdrs) { + rv = msgHdr->SetPriority(filterPriority); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + } + } break; + case nsMsgFilterAction::AddTag: { + nsCString keyword; + filterAction->GetStrValue(keyword); + rv = curFolder->AddKeywordsToMessages(m_searchHitHdrs, keyword); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + } break; + case nsMsgFilterAction::JunkScore: { + nsAutoCString junkScoreStr; + int32_t junkScore; + filterAction->GetJunkScore(&junkScore); + junkScoreStr.AppendInt(junkScore); + rv = + curFolder->SetJunkScoreForMessages(m_searchHitHdrs, junkScoreStr); + BREAK_ACTION_IF_FAILURE(rv, "Setting message flags failed"); + } break; + case nsMsgFilterAction::Forward: { + nsCOMPtr<nsIMsgIncomingServer> server; + rv = curFolder->GetServer(getter_AddRefs(server)); + BREAK_ACTION_IF_FAILURE(rv, "Could not get server"); + nsCString forwardTo; + filterAction->GetStrValue(forwardTo); + BREAK_ACTION_IF_FALSE(!forwardTo.IsEmpty(), "blank forwardTo URI"); + nsCOMPtr<nsIMsgComposeService> compService = + do_GetService("@mozilla.org/messengercompose;1", &rv); + BREAK_ACTION_IF_FAILURE(rv, "Could not get compose service"); + + for (auto msgHdr : m_searchHitHdrs) { + rv = compService->ForwardMessage( + NS_ConvertASCIItoUTF16(forwardTo), msgHdr, m_msgWindow, server, + nsIMsgComposeService::kForwardAsDefault); + BREAK_ACTION_IF_FAILURE(rv, "Forward action failed"); + } + } break; + case nsMsgFilterAction::Reply: { + nsCString replyTemplateUri; + filterAction->GetStrValue(replyTemplateUri); + BREAK_ACTION_IF_FALSE(!replyTemplateUri.IsEmpty(), + "Empty reply template URI"); + + nsCOMPtr<nsIMsgIncomingServer> server; + rv = curFolder->GetServer(getter_AddRefs(server)); + BREAK_ACTION_IF_FAILURE(rv, "Could not get server"); + + nsCOMPtr<nsIMsgComposeService> compService = + do_GetService("@mozilla.org/messengercompose;1", &rv); + BREAK_ACTION_IF_FAILURE(rv, "Could not get compose service"); + for (auto msgHdr : m_searchHitHdrs) { + rv = compService->ReplyWithTemplate(msgHdr, replyTemplateUri, + m_msgWindow, server); + if (NS_FAILED(rv)) { + if (rv == NS_ERROR_ABORT) { + (void)curFilter->LogRuleHitFail( + filterAction, msgHdr, rv, + "filterFailureSendingReplyAborted"_ns); + } else { + (void)curFilter->LogRuleHitFail( + filterAction, msgHdr, rv, + "filterFailureSendingReplyError"_ns); + } + } + BREAK_ACTION_IF_FAILURE(rv, "ReplyWithTemplate failed"); + } + } break; + case nsMsgFilterAction::DeleteFromPop3Server: { + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = + do_QueryInterface(curFolder, &rv); + BREAK_ACTION_IF_FAILURE(rv, "Current folder not a local folder"); + BREAK_ACTION_IF_FALSE(localFolder, + "Current folder not a local folder"); + // This action ignores the deleteMailLeftOnServer preference + rv = localFolder->MarkMsgsOnPop3Server(m_searchHitHdrs, + POP3_FORCE_DEL); + BREAK_ACTION_IF_FAILURE(rv, "MarkMsgsOnPop3Server failed"); + + // Delete the partial headers. They're useless now + // that the server copy is being deleted. + nsTArray<RefPtr<nsIMsgDBHdr>> partialMsgs; + for (uint32_t i = 0; i < m_searchHits.Length(); ++i) { + nsIMsgDBHdr* msgHdr = m_searchHitHdrs[i]; + nsMsgKey msgKey = m_searchHits[i]; + uint32_t flags; + msgHdr->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Partial) { + partialMsgs.AppendElement(msgHdr); + m_stopFiltering.AppendElement(msgKey); + curFolder->OrProcessingFlags(msgKey, + nsMsgProcessingFlags::FilterToMove); + } + } + if (!partialMsgs.IsEmpty()) { + rv = curFolder->DeleteMessages(partialMsgs, m_msgWindow, true, + false, nullptr, false); + BREAK_ACTION_IF_FAILURE(rv, "Delete messages failed"); + } + } break; + case nsMsgFilterAction::FetchBodyFromPop3Server: { + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = + do_QueryInterface(curFolder, &rv); + BREAK_ACTION_IF_FAILURE(rv, "current folder not local"); + BREAK_ACTION_IF_FALSE(localFolder, "current folder not local"); + nsTArray<RefPtr<nsIMsgDBHdr>> messages; + for (nsIMsgDBHdr* msgHdr : m_searchHitHdrs) { + uint32_t flags = 0; + msgHdr->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Partial) + messages.AppendElement(msgHdr); + } + if (messages.Length() > 0) { + rv = curFolder->DownloadMessagesForOffline(messages, m_msgWindow); + BREAK_ACTION_IF_FAILURE(rv, "DownloadMessagesForOffline failed"); + } + } break; + + case nsMsgFilterAction::StopExecution: { + // don't apply any more filters + m_stopFiltering.AppendElements(m_searchHits); + m_nextAction = numActions; + } break; + + case nsMsgFilterAction::Custom: { + nsMsgFilterTypeType filterType; + curFilter->GetFilterType(&filterType); + nsCOMPtr<nsIMsgFilterCustomAction> customAction; + rv = filterAction->GetCustomAction(getter_AddRefs(customAction)); + BREAK_ACTION_IF_FAILURE(rv, "Could not get custom action"); + + nsAutoCString value; + rv = filterAction->GetStrValue(value); + BREAK_ACTION_IF_FAILURE(rv, "Could not get custom action value"); + bool isAsync = false; + customAction->GetIsAsync(&isAsync); + rv = customAction->ApplyAction(m_searchHitHdrs, value, this, + filterType, m_msgWindow); + BREAK_ACTION_IF_FAILURE(rv, "custom action failed to apply"); + if (isAsync) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Action execution continues async")); + return NS_OK; // custom action should call ApplyFilter on callback + } + } break; + + default: + NS_ERROR("unexpected filter action"); + BREAK_ACTION_IF_FAILURE(NS_ERROR_UNEXPECTED, + "Unexpected filter action"); + } + if (NS_FAILED(finalResult)) { + mFinalResult = finalResult; + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, + ("(Post) Action execution failed with error: %" PRIx32, + static_cast<uint32_t>(mFinalResult))); + if (loggingEnabled && m_searchHitHdrs.Length() > 0) { + (void)curFilter->LogRuleHitFail(filterAction, m_searchHitHdrs[0], + mFinalResult, + "filterActionFailed"_ns); + } + } else { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Action execution succeeded")); + } + } + } while (false); // end error management block + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Finished executing actions")); + return RunNextFilter(); +} + +NS_IMETHODIMP nsMsgFilterService::GetTempFilterList( + nsIMsgFolder* aFolder, nsIMsgFilterList** aFilterList) { + NS_ENSURE_ARG_POINTER(aFilterList); + + nsMsgFilterList* filterList = new nsMsgFilterList; + filterList->SetFolder(aFolder); + filterList->m_temporaryList = true; + NS_ADDREF(*aFilterList = filterList); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilterService::ApplyFiltersToFolders( + nsIMsgFilterList* aFilterList, + const nsTArray<RefPtr<nsIMsgFolder>>& aFolders, nsIMsgWindow* aMsgWindow, + nsIMsgOperationListener* aCallback) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgFilterService::ApplyFiltersToFolders")); + NS_ENSURE_ARG_POINTER(aFilterList); + + uint32_t filterCount; + aFilterList->GetFilterCount(&filterCount); + nsCString listId; + aFilterList->GetListId(listId); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Manual filter run initiated")); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Running %" PRIu32 " filters from %s on %" PRIu32 " folders", + filterCount, listId.get(), (int)aFolders.Length())); + + RefPtr<nsMsgFilterAfterTheFact> filterExecutor = + new nsMsgFilterAfterTheFact(aMsgWindow, aFilterList, aFolders, aCallback); + if (filterExecutor) + return filterExecutor->AdvanceToNextFolder(); + else + return NS_ERROR_OUT_OF_MEMORY; +} + +NS_IMETHODIMP nsMsgFilterService::AddCustomAction( + nsIMsgFilterCustomAction* aAction) { + mCustomActions.AppendElement(aAction); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterService::GetCustomActions( + nsTArray<RefPtr<nsIMsgFilterCustomAction>>& actions) { + actions = mCustomActions.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilterService::GetCustomAction(const nsACString& aId, + nsIMsgFilterCustomAction** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + for (nsIMsgFilterCustomAction* action : mCustomActions) { + nsAutoCString id; + nsresult rv = action->GetId(id); + if (NS_SUCCEEDED(rv) && aId.Equals(id)) { + NS_ADDREF(*aResult = action); + return NS_OK; + } + } + aResult = nullptr; + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsMsgFilterService::AddCustomTerm(nsIMsgSearchCustomTerm* aTerm) { + mCustomTerms.AppendElement(aTerm); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterService::GetCustomTerms( + nsTArray<RefPtr<nsIMsgSearchCustomTerm>>& terms) { + terms = mCustomTerms.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFilterService::GetCustomTerm(const nsACString& aId, + nsIMsgSearchCustomTerm** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + for (nsIMsgSearchCustomTerm* term : mCustomTerms) { + nsAutoCString id; + nsresult rv = term->GetId(id); + if (NS_SUCCEEDED(rv) && aId.Equals(id)) { + NS_ADDREF(*aResult = term); + return NS_OK; + } + } + aResult = nullptr; + // we use a null result to indicate failure to find a term + return NS_OK; +} + +/** + * Translate the filter type flag into human readable type names. + * In case of multiple flag they are delimited by '&'. + */ +NS_IMETHODIMP +nsMsgFilterService::FilterTypeName(nsMsgFilterTypeType filterType, + nsACString& typeName) { + typeName.Truncate(); + if (filterType == nsMsgFilterType::None) { + typeName.Assign("None"); + return NS_OK; + } + + if ((filterType & nsMsgFilterType::Incoming) == nsMsgFilterType::Incoming) { + typeName.Append("Incoming&"); + } else { + if ((filterType & nsMsgFilterType::Inbox) == nsMsgFilterType::Inbox) { + typeName.Append("Inbox&"); + } else { + if (filterType & nsMsgFilterType::InboxRule) + typeName.Append("InboxRule&"); + if (filterType & nsMsgFilterType::InboxJavaScript) + typeName.Append("InboxJavaScript&"); + } + if ((filterType & nsMsgFilterType::News) == nsMsgFilterType::News) { + typeName.Append("News&"); + } else { + if (filterType & nsMsgFilterType::NewsRule) typeName.Append("NewsRule&"); + if (filterType & nsMsgFilterType::NewsJavaScript) + typeName.Append("NewsJavaScript&"); + } + } + if (filterType & nsMsgFilterType::Manual) typeName.Append("Manual&"); + if (filterType & nsMsgFilterType::PostPlugin) typeName.Append("PostPlugin&"); + if (filterType & nsMsgFilterType::PostOutgoing) + typeName.Append("PostOutgoing&"); + if (filterType & nsMsgFilterType::Archive) typeName.Append("Archive&"); + if (filterType & nsMsgFilterType::Periodic) typeName.Append("Periodic&"); + + if (typeName.IsEmpty()) { + typeName.Assign("UNKNOWN"); + } else { + // Cut the trailing '&' character. + typeName.Truncate(typeName.Length() - 1); + } + return NS_OK; +} + +// nsMsgApplyFiltersToMessages overrides nsMsgFilterAfterTheFact in order to +// apply filters to a list of messages, rather than an entire folder +class nsMsgApplyFiltersToMessages : public nsMsgFilterAfterTheFact { + public: + nsMsgApplyFiltersToMessages(nsIMsgWindow* aMsgWindow, + nsIMsgFilterList* aFilterList, + const nsTArray<RefPtr<nsIMsgFolder>>& aFolderList, + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgHdrList, + nsMsgFilterTypeType aFilterType, + nsIMsgOperationListener* aCallback); + + protected: + virtual nsresult RunNextFilter(); + + nsTArray<RefPtr<nsIMsgDBHdr>> m_msgHdrList; + nsMsgFilterTypeType m_filterType; +}; + +nsMsgApplyFiltersToMessages::nsMsgApplyFiltersToMessages( + nsIMsgWindow* aMsgWindow, nsIMsgFilterList* aFilterList, + const nsTArray<RefPtr<nsIMsgFolder>>& aFolderList, + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgHdrList, + nsMsgFilterTypeType aFilterType, nsIMsgOperationListener* aCallback) + : nsMsgFilterAfterTheFact(aMsgWindow, aFilterList, aFolderList, aCallback), + m_msgHdrList(aMsgHdrList.Clone()), + m_filterType(aFilterType) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgApplyFiltersToMessages")); +} + +nsresult nsMsgApplyFiltersToMessages::RunNextFilter() { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgApplyFiltersToMessages::RunNextFilter")); + nsresult rv = NS_OK; + while (true) { + m_curFilter = nullptr; // we are done with the current filter + if (!m_curFolder || // Not an error, we just need to run + // AdvanceToNextFolder() + m_curFilterIndex >= m_numFilters) + break; + + BREAK_IF_FALSE(m_filters, "No filters"); + nsMsgFilterTypeType filterType; + bool isEnabled; + rv = + m_filters->GetFilterAt(m_curFilterIndex++, getter_AddRefs(m_curFilter)); + CONTINUE_IF_FAILURE(rv, "Could not get filter"); + rv = m_curFilter->GetFilterType(&filterType); + CONTINUE_IF_FAILURE(rv, "Could not get filter type"); + if (!(filterType & m_filterType)) continue; + rv = m_curFilter->GetEnabled(&isEnabled); + CONTINUE_IF_FAILURE(rv, "Could not get isEnabled"); + if (!isEnabled) continue; + + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) Running filter %" PRIu32, m_curFilterIndex)); + nsString filterName; + m_curFilter->GetFilterName(filterName); + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Filter name: %s", NS_ConvertUTF16toUTF8(filterName).get())); + // clang-format on + + nsCOMPtr<nsIMsgSearchScopeTerm> scope(new nsMsgSearchScopeTerm( + nullptr, nsMsgSearchScope::offlineMail, m_curFolder)); + BREAK_IF_FALSE(scope, "Could not create scope, OOM?"); + m_curFilter->SetScope(scope); + OnNewSearch(); + + for (auto msgHdr : m_msgHdrList) { + bool matched; + rv = m_curFilter->MatchHdr(msgHdr, m_curFolder, m_curFolderDB, + EmptyCString(), &matched); + if (NS_SUCCEEDED(rv) && matched) { + // In order to work with nsMsgFilterAfterTheFact::ApplyFilter we + // initialize nsMsgFilterAfterTheFact's information with a search hit + // now for the message that we're filtering. + OnSearchHit(msgHdr, m_curFolder); + } + } + m_curFilter->SetScope(nullptr); + + if (m_searchHits.Length() > 0) { + m_nextAction = 0; + rv = ApplyFilter(); + if (NS_SUCCEEDED(rv)) + return NS_OK; // async callback will continue, or we are done. + } + } + + if (NS_FAILED(rv)) { + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, + ("(Post) Filter run failed (%" PRIx32 ")", + static_cast<uint32_t>(rv))); + // clang-format on + m_filters->LogFilterMessage(u"Filter run failed"_ns, m_curFilter); + NS_WARNING_ASSERTION(false, "Failed to run filters"); + } else { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Filter run finished on the current folder")); + } + + m_curFilter = nullptr; + + // We expect the failure is already recorded through one of the macro + // expressions, that will have console logging added to them. + // So an additional console warning is not needed here. + return AdvanceToNextFolder(); +} + +NS_IMETHODIMP nsMsgFilterService::ApplyFilters( + nsMsgFilterTypeType aFilterType, + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgHdrList, nsIMsgFolder* aFolder, + nsIMsgWindow* aMsgWindow, nsIMsgOperationListener* aCallback) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug, + ("(Post) nsMsgApplyFiltersToMessages::ApplyFilters")); + NS_ENSURE_ARG_POINTER(aFolder); + + nsCOMPtr<nsIMsgFilterList> filterList; + aFolder->GetFilterList(aMsgWindow, getter_AddRefs(filterList)); + NS_ENSURE_STATE(filterList); + + uint32_t filterCount; + filterList->GetFilterCount(&filterCount); + nsCString listId; + filterList->GetListId(listId); + nsString folderName; + aFolder->GetName(folderName); + nsCString typeName; + FilterTypeName(aFilterType, typeName); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Filter run initiated, trigger=%s (%i)", typeName.get(), + aFilterType)); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Running %" PRIu32 " filters from %s on %" PRIu32 + " message(s) in folder '%s'", + filterCount, listId.get(), (uint32_t)aMsgHdrList.Length(), + NS_ConvertUTF16toUTF8(folderName).get())); + + // Create our nsMsgApplyFiltersToMessages object which will be called when + // ApplyFiltersToHdr finds one or more filters that hit. + RefPtr<nsMsgApplyFiltersToMessages> filterExecutor = + new nsMsgApplyFiltersToMessages(aMsgWindow, filterList, {aFolder}, + aMsgHdrList, aFilterType, aCallback); + + if (filterExecutor) return filterExecutor->AdvanceToNextFolder(); + + return NS_ERROR_OUT_OF_MEMORY; +} + +/* void OnStartCopy (); */ +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnStartCopy() { return NS_OK; } + +/* void OnProgress (in uint32_t aProgress, in uint32_t aProgressMax); */ +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnProgress(uint32_t aProgress, + uint32_t aProgressMax) { + return NS_OK; +} + +/* void SetMessageKey (in uint32_t aKey); */ +NS_IMETHODIMP nsMsgFilterAfterTheFact::SetMessageKey(nsMsgKey /* aKey */) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilterAfterTheFact::GetMessageId(nsACString& messageId) { + return NS_OK; +} + +/* void OnStopCopy (in nsresult aStatus); */ +NS_IMETHODIMP nsMsgFilterAfterTheFact::OnStopCopy(nsresult aStatus) { + if (NS_SUCCEEDED(aStatus)) { + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("(Post) Async message copy from filter action finished successfully")); + // clang-format on + return ApplyFilter(); + } + MOZ_LOG(FILTERLOGMODULE, LogLevel::Error, + ("(Post) Async message copy from filter action failed (%" PRIx32 ")", + static_cast<uint32_t>(aStatus))); + + mFinalResult = aStatus; + if (m_msgWindow && !ContinueExecutionPrompt()) return OnEndExecution(); + + // Copy failed, so run the next filter + return RunNextFilter(); +} + +bool nsMsgFilterAfterTheFact::ContinueExecutionPrompt() { + if (!m_curFilter) return false; + nsCOMPtr<nsIStringBundle> bundle; + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + if (!bundleService) return false; + bundleService->CreateBundle("chrome://messenger/locale/filter.properties", + getter_AddRefs(bundle)); + if (!bundle) return false; + nsString filterName; + m_curFilter->GetFilterName(filterName); + nsString formatString; + nsString confirmText; + AutoTArray<nsString, 1> formatStrings = {filterName}; + nsresult rv = bundle->FormatStringFromName("continueFilterExecution", + formatStrings, confirmText); + if (NS_FAILED(rv)) return false; + bool returnVal = false; + (void)DisplayConfirmationPrompt(m_msgWindow, confirmText.get(), &returnVal); + if (!returnVal) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning, + ("(Post) User aborted further filter execution on prompt")); + } + return returnVal; +} + +nsresult nsMsgFilterAfterTheFact::DisplayConfirmationPrompt( + nsIMsgWindow* msgWindow, const char16_t* confirmString, bool* confirmed) { + if (msgWindow) { + nsCOMPtr<nsIDocShell> docShell; + msgWindow->GetRootDocShell(getter_AddRefs(docShell)); + if (docShell) { + nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell)); + if (dialog && confirmString) + dialog->Confirm(nullptr, confirmString, confirmed); + } + } + return NS_OK; +} |