/* -*- 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 "nsMsgBaseCID.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 "nsIOutputStream.h" #include "nsIStringBundle.h" #include "nsDateTimeFormatCID.h" #include "nsComponentManagerUtils.h" #include "nsServiceManagerUtils.h" #include "nsIMsgFilterService.h" #include "nsIMutableArray.h" #include "prmem.h" #include "mozilla/ArrayUtils.h" #include "mozilla/Services.h" static const char *kImapPrefix = "//imap:"; static const char *kWhitespace = "\b\t\r\n "; nsMsgRuleAction::nsMsgRuleAction() { } 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::SetLabel(nsMsgLabelValue aLabel) { NS_ENSURE_TRUE(m_type == nsMsgFilterAction::Label, NS_ERROR_ILLEGAL_VALUE); m_label = aLabel; return NS_OK; } NS_IMETHODIMP nsMsgRuleAction::GetLabel(nsMsgLabelValue *aResult) { NS_ENSURE_ARG_POINTER(aResult); NS_ENSURE_TRUE(m_type == nsMsgFilterAction::Label, NS_ERROR_ILLEGAL_VALUE); *aResult = m_label; 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(NS_MSGFILTERSERVICE_CONTRACTID, &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_temporary(false), m_unparseable(false), m_filterList(nullptr), m_expressionTree(nullptr) { nsresult rv = NS_NewISupportsArray(getter_AddRefs(m_termList)); if (NS_FAILED(rv)) NS_ASSERTION(false, "Failed to allocate a nsISupportsArray for nsMsgFilter"); m_type = nsMsgFilterType::InboxRule | nsMsgFilterType::Manual; } 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; return m_termList->AppendElement(static_cast(aTerm)); } NS_IMETHODIMP nsMsgFilter::CreateTerm(nsIMsgSearchTerm **aResult) { NS_ENSURE_ARG_POINTER(aResult); nsMsgSearchTerm *term = new nsMsgSearchTerm; NS_ENSURE_TRUE(term, NS_ERROR_OUT_OF_MEMORY); *aResult = static_cast(term); NS_ADDREF(*aResult); return NS_OK; } NS_IMETHODIMP nsMsgFilter::CreateAction(nsIMsgRuleAction **aAction) { NS_ENSURE_ARG_POINTER(aAction); nsMsgRuleAction *action = new nsMsgRuleAction; NS_ENSURE_TRUE(action, NS_ERROR_OUT_OF_MEMORY); *aAction = static_cast(action); NS_ADDREF(*aAction); 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(nsIArray **aActionList) { NS_ENSURE_ARG_POINTER(aActionList); uint32_t numActions; nsresult rv = GetActionCount(&numActions); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr orderedActions(do_CreateInstance(NS_ARRAY_CONTRACTID, &rv)); NS_ENSURE_SUCCESS(rv, rv); // hold separate pointers into the action list uint32_t nextIndexForNormal = 0, nextIndexForCopy = 0, nextIndexForMove = 0; for (uint32_t index = 0; index < numActions; ++index) { nsCOMPtr action; rv = GetActionAt(index, getter_AddRefs(action)); if (NS_FAILED(rv) || !action) continue; nsMsgRuleActionType actionType; action->GetType(&actionType); switch (actionType) { case nsMsgFilterAction::FetchBodyFromPop3Server: { // always insert in front rv = orderedActions->InsertElementAt(action, 0, false); NS_ENSURE_SUCCESS(rv, rv); ++nextIndexForNormal; ++nextIndexForCopy; ++nextIndexForMove; break; } case nsMsgFilterAction::CopyToFolder: { // insert into copy actions block, in order of appearance rv = orderedActions->InsertElementAt(action, nextIndexForCopy, false); NS_ENSURE_SUCCESS(rv, rv); ++nextIndexForCopy; ++nextIndexForMove; break; } case nsMsgFilterAction::MoveToFolder: case nsMsgFilterAction::Delete: { // insert into move/delete action block rv = orderedActions->InsertElementAt(action, nextIndexForMove, false); NS_ENSURE_SUCCESS(rv, rv); ++nextIndexForMove; break; } case nsMsgFilterAction::StopExecution: { // insert into stop action block rv = orderedActions->AppendElement(action, false); NS_ENSURE_SUCCESS(rv, rv); break; } default: { // insert into normal action block, in order of appearance rv = orderedActions->InsertElementAt(action, nextIndexForNormal, false); NS_ENSURE_SUCCESS(rv, rv); ++nextIndexForNormal; ++nextIndexForCopy; ++nextIndexForMove; break; } } } orderedActions.forget(aActionList); 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(*aAction = m_actionList[aIndex], NS_ERROR_ILLEGAL_VALUE); NS_IF_ADDREF(*aAction); 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 */ { nsCOMPtr term; nsresult rv = m_termList->QueryElementAt(termIndex, NS_GET_IID(nsIMsgSearchTerm), (void **)getter_AddRefs(term)); if (NS_SUCCEEDED(rv) && term) { 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(nsISupportsArray **aResult) { NS_ENSURE_ARG_POINTER(aResult); // caller can change m_termList, which can invalidate m_expressionTree. delete m_expressionTree; m_expressionTree = nullptr; NS_IF_ADDREF(*aResult = m_termList); return NS_OK; } NS_IMETHODIMP nsMsgFilter::SetSearchTerms(nsISupportsArray *aSearchList) { delete m_expressionTree; m_expressionTree = nullptr; m_termList = aSearchList; 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; } #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)) // Does this need to be localizable? #define LOG_ENTRY_TIMESTAMP "[$S] " // 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 char *aErrmsg) { NS_ENSURE_ARG_POINTER(aFilterAction); NS_ENSURE_ARG_POINTER(aMsgHdr); NS_ENSURE_TRUE(m_filterList, NS_OK); nsCOMPtr logStream; nsresult rv = m_filterList->GetLogStream(getter_AddRefs(logStream)); NS_ENSURE_SUCCESS(rv,rv); 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); if (!mDateFormatter) { mDateFormatter = do_CreateInstance(NS_DATETIMEFORMAT_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); if (!mDateFormatter) { return NS_ERROR_FAILURE; } } mDateFormatter->FormatPRExplodedTime(nullptr, kDateFormatShort, kTimeFormatSeconds, &exploded, dateValue); (void)aMsgHdr->GetMime2DecodedAuthor(authorValue); (void)aMsgHdr->GetMime2DecodedSubject(subjectValue); nsCString buffer; #ifdef MOZILLA_INTERNAL_API // 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); #endif nsCOMPtr bundleService = mozilla::services::GetStringBundleService(); NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); nsCOMPtr bundle; 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)) { // Let us put "Filter Action Failed: "%s" with error code=%s while attempting: " inside bundle. // 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) ; NS_ConvertASCIItoUTF16 tErrmsg16(aErrmsg) ; const char16_t *logErrorFormatStrings[2] = { tErrmsg16.get(), tcode16.get()}; nsString filterFailureWarningPrefix; rv = bundle->FormatStringFromName( u"filterFailureWarningPrefix", logErrorFormatStrings, 2, getter_Copies(filterFailureWarningPrefix)); NS_ENSURE_SUCCESS(rv, rv); buffer += NS_ConvertUTF16toUTF8(filterFailureWarningPrefix); buffer += "\n"; } const char16_t *filterLogDetectFormatStrings[4] = { filterName.get(), authorValue.get(), subjectValue.get(), dateValue.get() }; nsString filterLogDetectStr; rv = bundle->FormatStringFromName( u"filterLogDetectStr", filterLogDetectFormatStrings, 4, getter_Copies(filterLogDetectStr)); NS_ENSURE_SUCCESS(rv, rv); buffer += NS_ConvertUTF16toUTF8(filterLogDetectStr); buffer += "\n"; if (actionType == nsMsgFilterAction::MoveToFolder || actionType == nsMsgFilterAction::CopyToFolder) { nsCString actionFolderUri; aFilterAction->GetTargetFolderUri(actionFolderUri); NS_ConvertASCIItoUTF16 actionFolderUriValue(actionFolderUri); nsCString msgId; aMsgHdr->GetMessageId(getter_Copies(msgId)); NS_ConvertASCIItoUTF16 msgIdValue(msgId); const char16_t *logMoveFormatStrings[2] = { msgIdValue.get(), actionFolderUriValue.get() }; nsString logMoveStr; rv = bundle->FormatStringFromName( (actionType == nsMsgFilterAction::MoveToFolder) ? u"logMoveStr" : u"logCopyStr", logMoveFormatStrings, 2, getter_Copies(logMoveStr)); NS_ENSURE_SUCCESS(rv, rv); buffer += NS_ConvertUTF16toUTF8(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( u"filterMissingCustomAction", getter_Copies(filterActionName)); buffer += NS_ConvertUTF16toUTF8(filterActionName); } else { nsString actionValue; nsAutoString filterActionID; filterActionID = NS_LITERAL_STRING("filterAction"); filterActionID.AppendInt(actionType); rv = bundle->GetStringFromName(filterActionID.get(), getter_Copies(actionValue)); NS_ENSURE_SUCCESS(rv, rv); buffer += NS_ConvertUTF16toUTF8(actionValue); } buffer += "\n"; // Prepare timestamp PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded); mDateFormatter->FormatPRExplodedTime(nullptr, kDateFormatShort, kTimeFormatSeconds, &exploded, dateValue); nsCString timestampString(LOG_ENTRY_TIMESTAMP); MsgReplaceSubstring(timestampString, "$S", NS_ConvertUTF16toUTF8(dateValue).get()); // XXX: Finally, here we have enough context and buffer // (string) to display the filtering error if we want: for // example, a sticky error message in status bar, etc. uint32_t writeCount; rv = logStream->Write(LOG_ENTRY_START_TAG, LOG_ENTRY_START_TAG_LEN, &writeCount); NS_ENSURE_SUCCESS(rv,rv); NS_ASSERTION(writeCount == LOG_ENTRY_START_TAG_LEN, "failed to write out start log tag"); rv = logStream->Write(timestampString.get(), timestampString.Length(), &writeCount); NS_ENSURE_SUCCESS(rv,rv); NS_ASSERTION(writeCount == timestampString.Length(), "failed to write out timestamp"); // HTML-escape the log for security reasons. // We don't want someone to send us a message with a subject with // HTML tags, especially