diff options
Diffstat (limited to '')
-rw-r--r-- | comm/mailnews/search/src/nsMsgSearchTerm.cpp | 1797 |
1 files changed, 1797 insertions, 0 deletions
diff --git a/comm/mailnews/search/src/nsMsgSearchTerm.cpp b/comm/mailnews/search/src/nsMsgSearchTerm.cpp new file mode 100644 index 0000000000..7aad1628e5 --- /dev/null +++ b/comm/mailnews/search/src/nsMsgSearchTerm.cpp @@ -0,0 +1,1797 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "prmem.h" +#include "nsMsgSearchCore.h" +#include "nsIMsgSearchSession.h" +#include "nsMsgUtils.h" +#include "nsIMsgDatabase.h" +#include "nsIMsgHdr.h" +#include "nsMsgSearchTerm.h" +#include "nsMsgSearchScopeTerm.h" +#include "nsMsgBodyHandler.h" +#include "nsMsgResultElement.h" +#include "nsIMsgImapMailFolder.h" +#include "nsMsgSearchImap.h" +#include "nsMsgLocalSearch.h" +#include "nsMsgSearchNews.h" +#include "nsMsgSearchValue.h" +#include "nsMsgI18N.h" +#include "nsIMimeConverter.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsIMsgFilterPlugin.h" +#include "nsIFile.h" +#include "nsISeekableStream.h" +#include "nsNetCID.h" +#include "nsIFileStreams.h" +#include "nsUnicharUtils.h" +#include "nsIAbCard.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include <ctype.h> +#include "nsIMsgTagService.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgFilterService.h" +#include "nsIMsgPluggableStore.h" +#include "nsIAbManager.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/mailnews/MimeHeaderParser.h" +#include "mozilla/Utf8.h" + +using namespace mozilla::mailnews; + +//--------------------------------------------------------------------------- +// nsMsgSearchTerm specifies one criterion, e.g. name contains phil +//--------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +//-------------------- Implementation of nsMsgSearchTerm ----------------------- +//----------------------------------------------------------------------------- +#define MAILNEWS_CUSTOM_HEADERS "mailnews.customHeaders" + +typedef struct { + nsMsgSearchAttribValue attrib; + const char* attribName; +} nsMsgSearchAttribEntry; + +nsMsgSearchAttribEntry SearchAttribEntryTable[] = { + {nsMsgSearchAttrib::Subject, "subject"}, + {nsMsgSearchAttrib::Sender, "from"}, + {nsMsgSearchAttrib::Body, "body"}, + {nsMsgSearchAttrib::Date, "date"}, + {nsMsgSearchAttrib::Priority, "priority"}, + {nsMsgSearchAttrib::MsgStatus, "status"}, + {nsMsgSearchAttrib::To, "to"}, + {nsMsgSearchAttrib::CC, "cc"}, + {nsMsgSearchAttrib::ToOrCC, "to or cc"}, + {nsMsgSearchAttrib::AllAddresses, "all addresses"}, + {nsMsgSearchAttrib::AgeInDays, "age in days"}, + {nsMsgSearchAttrib::Keywords, "tag"}, + {nsMsgSearchAttrib::Size, "size"}, + // this used to be nsMsgSearchAttrib::SenderInAddressBook + // we used to have two Sender menuitems + // for backward compatibility, we can still parse + // the old style. see bug #179803 + {nsMsgSearchAttrib::Sender, "from in ab"}, + {nsMsgSearchAttrib::JunkStatus, "junk status"}, + {nsMsgSearchAttrib::JunkPercent, "junk percent"}, + {nsMsgSearchAttrib::JunkScoreOrigin, "junk score origin"}, + {nsMsgSearchAttrib::HasAttachmentStatus, "has attachment status"}, +}; + +static const unsigned int sNumSearchAttribEntryTable = + MOZ_ARRAY_LENGTH(SearchAttribEntryTable); + +// Take a string which starts off with an attribute +// and return the matching attribute. If the string is not in the table, and it +// begins with a quote, then we can conclude that it is an arbitrary header. +// Otherwise if not in the table, it is the id for a custom search term. +nsresult NS_MsgGetAttributeFromString(const char* string, + nsMsgSearchAttribValue* attrib, + nsACString& aCustomId) { + NS_ENSURE_ARG_POINTER(string); + NS_ENSURE_ARG_POINTER(attrib); + + bool found = false; + bool isArbitraryHeader = false; + // arbitrary headers have a leading quote + if (*string != '"') { + for (unsigned int idxAttrib = 0; idxAttrib < sNumSearchAttribEntryTable; + idxAttrib++) { + if (!PL_strcasecmp(string, + SearchAttribEntryTable[idxAttrib].attribName)) { + found = true; + *attrib = SearchAttribEntryTable[idxAttrib].attrib; + break; + } + } + } else // remove the leading quote + { + string++; + isArbitraryHeader = true; + } + + if (!found && !isArbitraryHeader) { + // must be a custom attribute + *attrib = nsMsgSearchAttrib::Custom; + aCustomId.Assign(string); + return NS_OK; + } + + if (!found) { + nsresult rv; + bool goodHdr; + IsRFC822HeaderFieldName(string, &goodHdr); + if (!goodHdr) return NS_MSG_INVALID_CUSTOM_HEADER; + // 49 is for showing customize... in ui, headers start from 50 onwards up + // until 99. + *attrib = nsMsgSearchAttrib::OtherHeader + 1; + + nsCOMPtr<nsIPrefService> prefService = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefBranch> prefBranch; + rv = prefService->GetBranch(nullptr, getter_AddRefs(prefBranch)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString headers; + prefBranch->GetCharPref(MAILNEWS_CUSTOM_HEADERS, headers); + + if (!headers.IsEmpty()) { + nsAutoCString hdrStr(headers); + hdrStr.StripWhitespace(); // remove whitespace before parsing + + char* newStr = hdrStr.BeginWriting(); + char* token = NS_strtok(":", &newStr); + uint32_t i = 0; + while (token) { + if (PL_strcasecmp(token, string) == 0) { + *attrib += i; // we found custom header in the pref + found = true; + break; + } + token = NS_strtok(":", &newStr); + i++; + } + } + } + // If we didn't find the header in MAILNEWS_CUSTOM_HEADERS, we're + // going to return NS_OK and an attrib of nsMsgSearchAttrib::OtherHeader+1. + // in case it's a client side spam filter description filter, + // which doesn't add its headers to mailnews.customMailHeaders. + // We've already checked that it's a valid header and returned + // an error if so. + + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchTerm::GetAttributeFromString( + const char* aString, nsMsgSearchAttribValue* aAttrib) { + nsAutoCString customId; + return NS_MsgGetAttributeFromString(aString, aAttrib, customId); +} + +nsresult NS_MsgGetStringForAttribute(int16_t attrib, const char** string) { + NS_ENSURE_ARG_POINTER(string); + + bool found = false; + for (unsigned int idxAttrib = 0; idxAttrib < sNumSearchAttribEntryTable; + idxAttrib++) { + // I'm using the idx's as aliases into MSG_SearchAttribute and + // MSG_SearchOperator enums which is legal because of the way the + // enums are defined (starts at 0, numItems at end) + if (attrib == SearchAttribEntryTable[idxAttrib].attrib) { + found = true; + *string = SearchAttribEntryTable[idxAttrib].attribName; + break; + } + } + if (!found) *string = ""; // don't leave the string uninitialized + + // we no longer return invalid attribute. If we cannot find the string in the + // table, then it is an arbitrary header. Return success regardless if found + // or not + return NS_OK; +} + +typedef struct { + nsMsgSearchOpValue op; + const char* opName; +} nsMsgSearchOperatorEntry; + +nsMsgSearchOperatorEntry SearchOperatorEntryTable[] = { + {nsMsgSearchOp::Contains, "contains"}, + {nsMsgSearchOp::DoesntContain, "doesn't contain"}, + {nsMsgSearchOp::Is, "is"}, + {nsMsgSearchOp::Isnt, "isn't"}, + {nsMsgSearchOp::IsEmpty, "is empty"}, + {nsMsgSearchOp::IsntEmpty, "isn't empty"}, + {nsMsgSearchOp::IsBefore, "is before"}, + {nsMsgSearchOp::IsAfter, "is after"}, + {nsMsgSearchOp::IsHigherThan, "is higher than"}, + {nsMsgSearchOp::IsLowerThan, "is lower than"}, + {nsMsgSearchOp::BeginsWith, "begins with"}, + {nsMsgSearchOp::EndsWith, "ends with"}, + {nsMsgSearchOp::IsInAB, "is in ab"}, + {nsMsgSearchOp::IsntInAB, "isn't in ab"}, + {nsMsgSearchOp::IsGreaterThan, "is greater than"}, + {nsMsgSearchOp::IsLessThan, "is less than"}, + {nsMsgSearchOp::Matches, "matches"}, + {nsMsgSearchOp::DoesntMatch, "doesn't match"}}; + +static const unsigned int sNumSearchOperatorEntryTable = + MOZ_ARRAY_LENGTH(SearchOperatorEntryTable); + +nsresult NS_MsgGetOperatorFromString(const char* string, int16_t* op) { + NS_ENSURE_ARG_POINTER(string); + NS_ENSURE_ARG_POINTER(op); + + bool found = false; + for (unsigned int idxOp = 0; idxOp < sNumSearchOperatorEntryTable; idxOp++) { + // I'm using the idx's as aliases into MSG_SearchAttribute and + // MSG_SearchOperator enums which is legal because of the way the + // enums are defined (starts at 0, numItems at end) + if (!PL_strcasecmp(string, SearchOperatorEntryTable[idxOp].opName)) { + found = true; + *op = SearchOperatorEntryTable[idxOp].op; + break; + } + } + return (found) ? NS_OK : NS_ERROR_INVALID_ARG; +} + +nsresult NS_MsgGetStringForOperator(int16_t op, const char** string) { + NS_ENSURE_ARG_POINTER(string); + + bool found = false; + for (unsigned int idxOp = 0; idxOp < sNumSearchOperatorEntryTable; idxOp++) { + // I'm using the idx's as aliases into MSG_SearchAttribute and + // MSG_SearchOperator enums which is legal because of the way the + // enums are defined (starts at 0, numItems at end) + if (op == SearchOperatorEntryTable[idxOp].op) { + found = true; + *string = SearchOperatorEntryTable[idxOp].opName; + break; + } + } + + return (found) ? NS_OK : NS_ERROR_INVALID_ARG; +} + +void NS_MsgGetUntranslatedStatusName(uint32_t s, nsCString* outName) { + const char* tmpOutName = NULL; +#define MSG_STATUS_MASK \ + (nsMsgMessageFlags::Read | nsMsgMessageFlags::Replied | \ + nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::New | \ + nsMsgMessageFlags::Marked) + uint32_t maskOut = (s & MSG_STATUS_MASK); + + // diddle the flags to pay attention to the most important ones first, if + // multiple flags are set. Should remove this code from the winfe. + if (maskOut & nsMsgMessageFlags::New) maskOut = nsMsgMessageFlags::New; + if (maskOut & nsMsgMessageFlags::Replied && + maskOut & nsMsgMessageFlags::Forwarded) + maskOut = nsMsgMessageFlags::Replied | nsMsgMessageFlags::Forwarded; + else if (maskOut & nsMsgMessageFlags::Forwarded) + maskOut = nsMsgMessageFlags::Forwarded; + else if (maskOut & nsMsgMessageFlags::Replied) + maskOut = nsMsgMessageFlags::Replied; + + switch (maskOut) { + case nsMsgMessageFlags::Read: + tmpOutName = "read"; + break; + case nsMsgMessageFlags::Replied: + tmpOutName = "replied"; + break; + case nsMsgMessageFlags::Forwarded: + tmpOutName = "forwarded"; + break; + case nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::Replied: + tmpOutName = "replied and forwarded"; + break; + case nsMsgMessageFlags::New: + tmpOutName = "new"; + break; + case nsMsgMessageFlags::Marked: + tmpOutName = "flagged"; + break; + default: + // This is fine, status may be "unread" for example + break; + } + + if (tmpOutName) *outName = tmpOutName; +} + +int32_t NS_MsgGetStatusValueFromName(char* name) { + if (!strcmp("read", name)) return nsMsgMessageFlags::Read; + if (!strcmp("replied", name)) return nsMsgMessageFlags::Replied; + if (!strcmp("forwarded", name)) return nsMsgMessageFlags::Forwarded; + if (!strcmp("replied and forwarded", name)) + return nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::Replied; + if (!strcmp("new", name)) return nsMsgMessageFlags::New; + if (!strcmp("flagged", name)) return nsMsgMessageFlags::Marked; + return 0; +} + +// Needed for DeStream method. +nsMsgSearchTerm::nsMsgSearchTerm() { + // initialize this to zero + m_value.attribute = 0; + m_value.u.priority = 0; + m_attribute = nsMsgSearchAttrib::Default; + m_operator = nsMsgSearchOp::Contains; + mBeginsGrouping = false; + mEndsGrouping = false; + m_matchAll = false; + + // valgrind warning during GC/java data check suggests + // m_booleanp needs to be initialized too. + m_booleanOp = nsMsgSearchBooleanOp::BooleanAND; +} + +nsMsgSearchTerm::nsMsgSearchTerm(nsMsgSearchAttribValue attrib, + nsMsgSearchOpValue op, nsIMsgSearchValue* val, + nsMsgSearchBooleanOperator boolOp, + const char* aCustomString) { + m_operator = op; + m_attribute = attrib; + m_booleanOp = boolOp; + if (attrib > nsMsgSearchAttrib::OtherHeader && + attrib < nsMsgSearchAttrib::kNumMsgSearchAttributes && aCustomString) { + m_arbitraryHeader = aCustomString; + ToLowerCaseExceptSpecials(m_arbitraryHeader); + } else if (attrib == nsMsgSearchAttrib::Custom) { + m_customId = aCustomString; + } + + nsMsgResultElement::AssignValues(val, &m_value); + m_matchAll = false; + mBeginsGrouping = false; + mEndsGrouping = false; +} + +nsMsgSearchTerm::~nsMsgSearchTerm() {} + +NS_IMPL_ISUPPORTS(nsMsgSearchTerm, nsIMsgSearchTerm) + +// Perhaps we could find a better place for this? +// Caller needs to free. +/* static */ char* nsMsgSearchTerm::EscapeQuotesInStr(const char* str) { + int numQuotes = 0; + for (const char* strPtr = str; *strPtr; strPtr++) + if (*strPtr == '"') numQuotes++; + int escapedStrLen = PL_strlen(str) + numQuotes; + char* escapedStr = (char*)PR_Malloc(escapedStrLen + 1); + if (escapedStr) { + char* destPtr; + for (destPtr = escapedStr; *str; str++) { + if (*str == '"') *destPtr++ = '\\'; + *destPtr++ = *str; + } + *destPtr = '\0'; + } + return escapedStr; +} + +nsresult nsMsgSearchTerm::OutputValue(nsCString& outputStr) { + if (IS_STRING_ATTRIBUTE(m_attribute) && !m_value.utf8String.IsEmpty()) { + bool quoteVal = false; + // need to quote strings with ')' and strings starting with '"' or ' ' + // filter code will escape quotes + if (m_value.utf8String.FindChar(')') != kNotFound || + (m_value.utf8String.First() == ' ') || + (m_value.utf8String.First() == '"')) { + quoteVal = true; + outputStr += "\""; + } + if (m_value.utf8String.FindChar('"') != kNotFound) { + char* escapedString = + nsMsgSearchTerm::EscapeQuotesInStr(m_value.utf8String.get()); + if (escapedString) { + outputStr += escapedString; + PR_Free(escapedString); + } + + } else { + outputStr += m_value.utf8String; + } + if (quoteVal) outputStr += "\""; + } else { + switch (m_attribute) { + case nsMsgSearchAttrib::Date: { + PRExplodedTime exploded; + PR_ExplodeTime(m_value.u.date, PR_LocalTimeParameters, &exploded); + + // wow, so tm_mon is 0 based, tm_mday is 1 based. + char dateBuf[100]; + PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%d-%b-%Y", &exploded); + outputStr += dateBuf; + break; + } + case nsMsgSearchAttrib::AgeInDays: { + outputStr.AppendInt(m_value.u.age); + break; + } + case nsMsgSearchAttrib::JunkStatus: { + outputStr.AppendInt( + m_value.u.junkStatus); // only if we write to disk, right? + break; + } + case nsMsgSearchAttrib::JunkPercent: { + outputStr.AppendInt(m_value.u.junkPercent); + break; + } + case nsMsgSearchAttrib::MsgStatus: { + nsAutoCString status; + NS_MsgGetUntranslatedStatusName(m_value.u.msgStatus, &status); + outputStr += status; + break; + } + case nsMsgSearchAttrib::Priority: { + nsAutoCString priority; + NS_MsgGetUntranslatedPriorityName(m_value.u.priority, priority); + outputStr += priority; + break; + } + case nsMsgSearchAttrib::HasAttachmentStatus: { + outputStr.AppendLiteral("true"); // don't need anything here, really + break; + } + case nsMsgSearchAttrib::Size: { + outputStr.AppendInt(m_value.u.size); + break; + } + case nsMsgSearchAttrib::Uint32HdrProperty: { + outputStr.AppendInt(m_value.u.msgStatus); + break; + } + default: + NS_ASSERTION(false, "trying to output invalid attribute"); + break; + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchTerm::GetTermAsString(nsACString& outStream) { + const char* operatorStr; + nsAutoCString outputStr; + nsresult rv; + + if (m_matchAll) { + outStream = "ALL"; + return NS_OK; + } + + // if arbitrary header, use it instead! + if (m_attribute > nsMsgSearchAttrib::OtherHeader && + m_attribute < nsMsgSearchAttrib::kNumMsgSearchAttributes) { + outputStr = "\""; + outputStr += m_arbitraryHeader; + outputStr += "\""; + } else if (m_attribute == nsMsgSearchAttrib::Custom) { + // use the custom id as the string + outputStr = m_customId; + } + + else if (m_attribute == nsMsgSearchAttrib::Uint32HdrProperty) { + outputStr = m_hdrProperty; + } else { + const char* attrib; + rv = NS_MsgGetStringForAttribute(m_attribute, &attrib); + NS_ENSURE_SUCCESS(rv, rv); + + outputStr = attrib; + } + + outputStr += ','; + + rv = NS_MsgGetStringForOperator(m_operator, &operatorStr); + NS_ENSURE_SUCCESS(rv, rv); + + outputStr += operatorStr; + outputStr += ','; + + OutputValue(outputStr); + outStream = outputStr; + return NS_OK; +} + +// fill in m_value from the input stream. +nsresult nsMsgSearchTerm::ParseValue(char* inStream) { + if (IS_STRING_ATTRIBUTE(m_attribute)) { + bool quoteVal = false; + while (isspace(*inStream)) inStream++; + // need to remove pair of '"', if present + if (*inStream == '"') { + quoteVal = true; + inStream++; + } + int valueLen = PL_strlen(inStream); + if (quoteVal && inStream[valueLen - 1] == '"') valueLen--; + + m_value.utf8String.Assign(inStream, valueLen); + CopyUTF8toUTF16(m_value.utf8String, m_value.utf16String); + } else { + switch (m_attribute) { + case nsMsgSearchAttrib::Date: + PR_ParseTimeString(inStream, false, &m_value.u.date); + break; + case nsMsgSearchAttrib::MsgStatus: + m_value.u.msgStatus = NS_MsgGetStatusValueFromName(inStream); + break; + case nsMsgSearchAttrib::Priority: + NS_MsgGetPriorityFromString(inStream, m_value.u.priority); + break; + case nsMsgSearchAttrib::AgeInDays: + m_value.u.age = atoi(inStream); + break; + case nsMsgSearchAttrib::JunkStatus: + m_value.u.junkStatus = + atoi(inStream); // only if we read from disk, right? + break; + case nsMsgSearchAttrib::JunkPercent: + m_value.u.junkPercent = atoi(inStream); + break; + case nsMsgSearchAttrib::HasAttachmentStatus: + m_value.u.msgStatus = nsMsgMessageFlags::Attachment; + break; // this should always be true. + case nsMsgSearchAttrib::Size: + m_value.u.size = atoi(inStream); + break; + default: + NS_ASSERTION(false, "invalid attribute parsing search term value"); + break; + } + } + m_value.attribute = m_attribute; + return NS_OK; +} + +// find the operator code for this operator string. +nsresult nsMsgSearchTerm::ParseOperator(char* inStream, + nsMsgSearchOpValue* value) { + NS_ENSURE_ARG_POINTER(value); + int16_t operatorVal; + while (isspace(*inStream)) inStream++; + + char* commaSep = PL_strchr(inStream, ','); + + if (commaSep) *commaSep = '\0'; + + nsresult rv = NS_MsgGetOperatorFromString(inStream, &operatorVal); + if (NS_SUCCEEDED(rv)) *value = (nsMsgSearchOpValue)operatorVal; + return rv; +} + +// find the attribute code for this comma-delimited attribute. +nsresult nsMsgSearchTerm::ParseAttribute(char* inStream, + nsMsgSearchAttribValue* attrib) { + while (isspace(*inStream)) inStream++; + + // if we are dealing with an arbitrary header, it will be quoted.... + // it seems like a kludge, but to distinguish arbitrary headers from + // standard headers with the same name, like "Date", we'll use the + // presence of the quote to recognize arbitrary headers. We leave the + // leading quote as a flag, but remove the trailing quote. + bool quoteVal = false; + if (*inStream == '"') quoteVal = true; + + // arbitrary headers are quoted. Skip first character, which will be the + // first quote for arbitrary headers + char* separator = strchr(inStream + 1, quoteVal ? '"' : ','); + + if (separator) *separator = '\0'; + + nsAutoCString customId; + nsresult rv = NS_MsgGetAttributeFromString(inStream, attrib, m_customId); + NS_ENSURE_SUCCESS(rv, rv); + + if (*attrib > nsMsgSearchAttrib::OtherHeader && + *attrib < nsMsgSearchAttrib::kNumMsgSearchAttributes) { + // We are dealing with an arbitrary header. + m_arbitraryHeader = inStream + 1; // remove the leading quote + ToLowerCaseExceptSpecials(m_arbitraryHeader); + } + return rv; +} + +// De stream one search term. If the condition looks like +// condition = "(to or cc, contains, r-thompson) AND (body, doesn't contain, +// fred)" This routine should get called twice, the first time with "to or cc, +// contains, r-thompson", the second time with "body, doesn't contain, fred" + +nsresult nsMsgSearchTerm::DeStreamNew(char* inStream, int16_t /*length*/) { + if (!strcmp(inStream, "ALL")) { + m_matchAll = true; + return NS_OK; + } + char* commaSep = PL_strchr(inStream, ','); + nsresult rv = ParseAttribute( + inStream, + &m_attribute); // will allocate space for arbitrary header if necessary + NS_ENSURE_SUCCESS(rv, rv); + if (!commaSep) return NS_ERROR_INVALID_ARG; + char* secondCommaSep = PL_strchr(commaSep + 1, ','); + if (commaSep) rv = ParseOperator(commaSep + 1, &m_operator); + NS_ENSURE_SUCCESS(rv, rv); + // convert label filters and saved searches to keyword equivalents + if (secondCommaSep) ParseValue(secondCommaSep + 1); + return NS_OK; +} + +// Looks in the MessageDB for the user specified arbitrary header, if it finds +// the header, it then looks for a match against the value for the header. +nsresult nsMsgSearchTerm::MatchArbitraryHeader( + nsIMsgSearchScopeTerm* scope, uint32_t length /* in lines*/, + const char* charset, bool charsetOverride, nsIMsgDBHdr* msg, + nsIMsgDatabase* db, const nsACString& headers, bool ForFiltering, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + *pResult = false; + nsresult rv = NS_OK; + bool matchExpected = m_operator == nsMsgSearchOp::Contains || + m_operator == nsMsgSearchOp::Is || + m_operator == nsMsgSearchOp::BeginsWith || + m_operator == nsMsgSearchOp::EndsWith; + // Initialize result to what we want if we don't find the header at all. + bool result = !matchExpected; + + nsCString dbHdrValue; + msg->GetStringProperty(m_arbitraryHeader.get(), dbHdrValue); + if (!dbHdrValue.IsEmpty()) { + // Match value with the other info. It doesn't check all header occurrences, + // so we use it only if we match and do line by line headers parsing + // otherwise. + rv = MatchRfc2047String(dbHdrValue, charset, charsetOverride, pResult); + if (matchExpected == *pResult) return rv; + + // Preset result in case we don't have access to the headers, like for IMAP. + result = *pResult; + } + + nsMsgBodyHandler* bodyHandler = + new nsMsgBodyHandler(scope, length, msg, db, headers.BeginReading(), + headers.Length(), ForFiltering); + bodyHandler->SetStripHeaders(false); + + nsCString headerFullValue; // Contains matched header value accumulated over + // multiple lines. + nsAutoCString buf; + nsAutoCString curMsgHeader; + bool processingHeaders = true; + + while (processingHeaders) { + nsCString charsetIgnored; + if (bodyHandler->GetNextLine(buf, charsetIgnored) < 0 || + EMPTY_MESSAGE_LINE(buf)) + processingHeaders = + false; // No more lines or empty line terminating headers. + + bool isContinuationHeader = + processingHeaders ? NS_IsAsciiWhitespace(buf.CharAt(0)) : false; + + // If we're not on a continuation header the header value is not empty, + // we have finished accumulating the header value by iterating over all + // header lines. Now we need to check whether the value is a match. + if (!isContinuationHeader && !headerFullValue.IsEmpty()) { + bool stringMatches; + // Match value with the other info. + rv = MatchRfc2047String(headerFullValue, charset, charsetOverride, + &stringMatches); + if (matchExpected == stringMatches) // if we found a match + { + // If we found a match, stop examining the headers. + processingHeaders = false; + result = stringMatches; + } + // Prepare for repeated header of the same type. + headerFullValue.Truncate(); + } + + // We got result or finished processing all lines. + if (!processingHeaders) break; + + char* buf_end = (char*)(buf.get() + buf.Length()); + int headerLength = m_arbitraryHeader.Length(); + + // If the line starts with whitespace, then we use the current header. + if (!isContinuationHeader) { + // Here we start a new header. + uint32_t colonPos = buf.FindChar(':'); + curMsgHeader = StringHead(buf, colonPos); + } + + if (curMsgHeader.Equals(m_arbitraryHeader, + nsCaseInsensitiveCStringComparator)) { + // Process the value: + // Value occurs after the header name or whitespace continuation char. + const char* headerValue = + buf.get() + (isContinuationHeader ? 1 : headerLength); + if (headerValue < buf_end && + headerValue[0] == + ':') // + 1 to account for the colon which is MANDATORY + headerValue++; + + // Strip leading white space. + while (headerValue < buf_end && isspace(*headerValue)) headerValue++; + + // Strip trailing white space. + char* end = buf_end - 1; + while (headerValue < end && isspace(*end)) { + *end = '\0'; + end--; + } + + // Any continuation whitespace is converted to a single space. + if (!headerFullValue.IsEmpty()) headerFullValue.Append(' '); + headerFullValue.Append(nsDependentCString(headerValue)); + } + } + + delete bodyHandler; + *pResult = result; + return rv; +} + +NS_IMETHODIMP nsMsgSearchTerm::MatchHdrProperty(nsIMsgDBHdr* aHdr, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_ENSURE_ARG_POINTER(aHdr); + + nsCString dbHdrValue; + aHdr->GetStringProperty(m_hdrProperty.get(), dbHdrValue); + return MatchString(dbHdrValue, nullptr, aResult); +} + +NS_IMETHODIMP nsMsgSearchTerm::MatchFolderFlag(nsIMsgDBHdr* aMsgToMatch, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aMsgToMatch); + NS_ENSURE_ARG_POINTER(aResult); + + nsCOMPtr<nsIMsgFolder> msgFolder; + nsresult rv = aMsgToMatch->GetFolder(getter_AddRefs(msgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t folderFlags; + msgFolder->GetFlags(&folderFlags); + return MatchStatus(folderFlags, aResult); +} + +NS_IMETHODIMP nsMsgSearchTerm::MatchUint32HdrProperty(nsIMsgDBHdr* aHdr, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_ENSURE_ARG_POINTER(aHdr); + + nsresult rv = NS_OK; + uint32_t dbHdrValue; + aHdr->GetUint32Property(m_hdrProperty.get(), &dbHdrValue); + + bool result = false; + switch (m_operator) { + case nsMsgSearchOp::IsGreaterThan: + if (dbHdrValue > m_value.u.msgStatus) result = true; + break; + case nsMsgSearchOp::IsLessThan: + if (dbHdrValue < m_value.u.msgStatus) result = true; + break; + case nsMsgSearchOp::Is: + if (dbHdrValue == m_value.u.msgStatus) result = true; + break; + case nsMsgSearchOp::Isnt: + if (dbHdrValue != m_value.u.msgStatus) result = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for uint"); + } + *aResult = result; + return rv; +} + +nsresult nsMsgSearchTerm::MatchBody(nsIMsgSearchScopeTerm* scope, + uint64_t offset, + uint32_t length /*in lines*/, + const char* folderCharset, nsIMsgDBHdr* msg, + nsIMsgDatabase* db, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv = NS_OK; + + bool result = false; + *pResult = false; + + // Small hack so we don't look all through a message when someone has + // specified "BODY IS foo". ### Since length is in lines, this is not quite + // right. + if ((length > 0) && + (m_operator == nsMsgSearchOp::Is || m_operator == nsMsgSearchOp::Isnt)) + length = m_value.utf8String.Length(); + + nsMsgBodyHandler* bodyHan = new nsMsgBodyHandler(scope, length, msg, db); + if (!bodyHan) return NS_ERROR_OUT_OF_MEMORY; + + nsAutoCString buf; + bool endOfFile = false; // if retValue == 0, we've hit the end of the file + + // Change the sense of the loop so we don't bail out prematurely + // on negative terms. i.e. opDoesntContain must look at all lines + bool boolContinueLoop; + GetMatchAllBeforeDeciding(&boolContinueLoop); + result = boolContinueLoop; + + nsCString compare; + nsCString charset; + while (!endOfFile && result == boolContinueLoop) { + if (bodyHan->GetNextLine(buf, charset) >= 0) { + bool softLineBreak = false; + // Do in-place decoding of quoted printable + if (bodyHan->IsQP()) { + softLineBreak = StringEndsWith(buf, "="_ns); + MsgStripQuotedPrintable(buf); + // If soft line break, chop off the last char as well. + size_t bufLength = buf.Length(); + if ((bufLength > 0) && softLineBreak) buf.SetLength(bufLength - 1); + } + compare.Append(buf); + // If this line ends with a soft line break, loop around + // and get the next line before looking for the search string. + // This assumes the message can't end on a QP soft line break. + // That seems like a pretty safe assumption. + if (softLineBreak) continue; + if (!compare.IsEmpty()) { + char startChar = (char)compare.CharAt(0); + if (startChar != '\r' && startChar != '\n') { + rv = MatchString(compare, + charset.IsEmpty() ? folderCharset : charset.get(), + &result); + } + compare.Truncate(); + } + } else + endOfFile = true; + } + + delete bodyHan; + *pResult = result; + return rv; +} + +nsresult nsMsgSearchTerm::InitializeAddressBook() { + // the search attribute value has the URI for the address book we need to + // load. we need both the database and the directory. + nsresult rv = NS_OK; + + if (mDirectory) { + nsCString uri; + rv = mDirectory->GetURI(uri); + NS_ENSURE_SUCCESS(rv, rv); + + if (!uri.Equals(m_value.utf8String)) + // clear out the directory....we are no longer pointing to the right one + mDirectory = nullptr; + } + if (!mDirectory) { + nsCOMPtr<nsIAbManager> abManager = + do_GetService("@mozilla.org/abmanager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = + abManager->GetDirectory(m_value.utf8String, getter_AddRefs(mDirectory)); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult nsMsgSearchTerm::MatchInAddressBook(const nsAString& aAddress, + bool* pResult) { + nsresult rv = InitializeAddressBook(); + *pResult = false; + + // Some junkmails have empty From: fields. + if (aAddress.IsEmpty()) return rv; + + if (mDirectory) { + nsCOMPtr<nsIAbCard> cardForAddress = nullptr; + rv = mDirectory->CardForEmailAddress(NS_ConvertUTF16toUTF8(aAddress), + getter_AddRefs(cardForAddress)); + if (NS_FAILED(rv) && rv != NS_ERROR_NOT_IMPLEMENTED) return rv; + switch (m_operator) { + case nsMsgSearchOp::IsInAB: + if (cardForAddress) *pResult = true; + break; + case nsMsgSearchOp::IsntInAB: + if (!cardForAddress) *pResult = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for address book"); + } + } + + return rv; +} + +// *pResult is false when strings don't match, true if they do. +nsresult nsMsgSearchTerm::MatchRfc2047String(const nsACString& rfc2047string, + const char* charset, + bool charsetOverride, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv; + nsCOMPtr<nsIMimeConverter> mimeConverter = + do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoString stringToMatch; + rv = mimeConverter->DecodeMimeHeader(PromiseFlatCString(rfc2047string).get(), + charset, charsetOverride, false, + stringToMatch); + NS_ENSURE_SUCCESS(rv, rv); + if (m_operator == nsMsgSearchOp::IsInAB || + m_operator == nsMsgSearchOp::IsntInAB) + return MatchInAddressBook(stringToMatch, pResult); + + return MatchString(stringToMatch, pResult); +} + +// *pResult is false when strings don't match, true if they do. +nsresult nsMsgSearchTerm::MatchString(const nsACString& stringToMatch, + const char* charset, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + bool result = false; + + nsresult rv = NS_OK; + + // Save some performance for opIsEmpty / opIsntEmpty + if (nsMsgSearchOp::IsEmpty == m_operator) { + if (stringToMatch.IsEmpty()) result = true; + } else if (nsMsgSearchOp::IsntEmpty == m_operator) { + if (!stringToMatch.IsEmpty()) result = true; + } else { + nsAutoString utf16StrToMatch; + rv = NS_ERROR_UNEXPECTED; + if (charset) { + rv = nsMsgI18NConvertToUnicode(nsDependentCString(charset), stringToMatch, + utf16StrToMatch); + } + if (NS_FAILED(rv)) { + // No charset or conversion failed, maybe due to a bad charset, try UTF-8. + if (mozilla::IsUtf8(stringToMatch)) { + CopyUTF8toUTF16(stringToMatch, utf16StrToMatch); + } else { + // Bad luck, let's assume ASCII/windows-1252 then. + CopyASCIItoUTF16(stringToMatch, utf16StrToMatch); + } + } + + rv = MatchString(utf16StrToMatch, &result); + } + + *pResult = result; + return rv; +} + +// *pResult is false when strings don't match, true if they do. +nsresult nsMsgSearchTerm::MatchString(const nsAString& utf16StrToMatch, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + bool result = false; + + nsresult rv = NS_OK; + auto needle = m_value.utf16String; + + switch (m_operator) { + case nsMsgSearchOp::Contains: + if (CaseInsensitiveFindInReadable(needle, utf16StrToMatch)) result = true; + break; + case nsMsgSearchOp::DoesntContain: + if (!CaseInsensitiveFindInReadable(needle, utf16StrToMatch)) + result = true; + break; + case nsMsgSearchOp::Is: + if (needle.Equals(utf16StrToMatch, nsCaseInsensitiveStringComparator)) + result = true; + break; + case nsMsgSearchOp::Isnt: + if (!needle.Equals(utf16StrToMatch, nsCaseInsensitiveStringComparator)) + result = true; + break; + case nsMsgSearchOp::IsEmpty: + if (utf16StrToMatch.IsEmpty()) result = true; + break; + case nsMsgSearchOp::IsntEmpty: + if (!utf16StrToMatch.IsEmpty()) result = true; + break; + case nsMsgSearchOp::BeginsWith: + if (StringBeginsWith(utf16StrToMatch, needle, + nsCaseInsensitiveStringComparator)) + result = true; + break; + case nsMsgSearchOp::EndsWith: + if (StringEndsWith(utf16StrToMatch, needle, + nsCaseInsensitiveStringComparator)) + result = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for matching search results"); + } + + *pResult = result; + return rv; +} + +NS_IMETHODIMP nsMsgSearchTerm::GetMatchAllBeforeDeciding(bool* aResult) { + *aResult = (m_operator == nsMsgSearchOp::DoesntContain || + m_operator == nsMsgSearchOp::Isnt); + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchTerm::MatchRfc822String(const nsACString& string, + const char* charset, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + *pResult = false; + bool result; + + // Change the sense of the loop so we don't bail out prematurely + // on negative terms. i.e. opDoesntContain must look at all recipients + bool boolContinueLoop; + GetMatchAllBeforeDeciding(&boolContinueLoop); + result = boolContinueLoop; + + // If the operator is Contains, then we can cheat and avoid having to parse + // addresses. This does open up potential spurious matches for punctuation + // (e.g., ; or <), but the likelihood of users intending to search for these + // and also being able to match them is rather low. This optimization is not + // applicable to any other search type. + if (m_operator == nsMsgSearchOp::Contains) + return MatchRfc2047String(string, charset, false, pResult); + + nsTArray<nsString> names, addresses; + ExtractAllAddresses(EncodedHeader(string, charset), names, addresses); + uint32_t count = names.Length(); + + nsresult rv = NS_OK; + for (uint32_t i = 0; i < count && result == boolContinueLoop; i++) { + if (m_operator == nsMsgSearchOp::IsInAB || + m_operator == nsMsgSearchOp::IsntInAB) { + rv = MatchInAddressBook(addresses[i], &result); + } else { + rv = MatchString(names[i], &result); + if (boolContinueLoop == result) rv = MatchString(addresses[i], &result); + } + } + *pResult = result; + return rv; +} + +nsresult nsMsgSearchTerm::GetLocalTimes(PRTime a, PRTime b, + PRExplodedTime& aExploded, + PRExplodedTime& bExploded) { + PR_ExplodeTime(a, PR_LocalTimeParameters, &aExploded); + PR_ExplodeTime(b, PR_LocalTimeParameters, &bExploded); + return NS_OK; +} + +nsresult nsMsgSearchTerm::MatchDate(PRTime dateToMatch, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv = NS_OK; + bool result = false; + + PRExplodedTime tmToMatch, tmThis; + if (NS_SUCCEEDED( + GetLocalTimes(dateToMatch, m_value.u.date, tmToMatch, tmThis))) { + switch (m_operator) { + case nsMsgSearchOp::IsBefore: + if (tmToMatch.tm_year < tmThis.tm_year || + (tmToMatch.tm_year == tmThis.tm_year && + tmToMatch.tm_yday < tmThis.tm_yday)) + result = true; + break; + case nsMsgSearchOp::IsAfter: + if (tmToMatch.tm_year > tmThis.tm_year || + (tmToMatch.tm_year == tmThis.tm_year && + tmToMatch.tm_yday > tmThis.tm_yday)) + result = true; + break; + case nsMsgSearchOp::Is: + if (tmThis.tm_year == tmToMatch.tm_year && + tmThis.tm_month == tmToMatch.tm_month && + tmThis.tm_mday == tmToMatch.tm_mday) + result = true; + break; + case nsMsgSearchOp::Isnt: + if (tmThis.tm_year != tmToMatch.tm_year || + tmThis.tm_month != tmToMatch.tm_month || + tmThis.tm_mday != tmToMatch.tm_mday) + result = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for dates"); + } + } + *pResult = result; + return rv; +} + +nsresult nsMsgSearchTerm::MatchAge(PRTime msgDate, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + bool result = false; + nsresult rv = NS_OK; + + PRTime now = PR_Now(); + PRTime cutOffDay = now - m_value.u.age * PR_USEC_PER_DAY; + + bool cutOffDayInTheFuture = m_value.u.age < 0; + + // So now cutOffDay is the PRTime cut-off point. + // Any msg with a time less than that will be past the age. + + switch (m_operator) { + case nsMsgSearchOp::IsGreaterThan: // is older than, or more in the future + if ((!cutOffDayInTheFuture && msgDate < cutOffDay) || + (cutOffDayInTheFuture && msgDate > cutOffDay)) + result = true; + break; + case nsMsgSearchOp::IsLessThan: // is younger than, or less in the future + if ((!cutOffDayInTheFuture && msgDate > cutOffDay) || + (cutOffDayInTheFuture && msgDate < cutOffDay)) + result = true; + break; + case nsMsgSearchOp::Is: + PRExplodedTime msgDateExploded; + PRExplodedTime cutOffDayExploded; + if (NS_SUCCEEDED(GetLocalTimes(msgDate, cutOffDay, msgDateExploded, + cutOffDayExploded))) { + if ((msgDateExploded.tm_mday == cutOffDayExploded.tm_mday) && + (msgDateExploded.tm_month == cutOffDayExploded.tm_month) && + (msgDateExploded.tm_year == cutOffDayExploded.tm_year)) + result = true; + } + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for msg age"); + } + *pResult = result; + return rv; +} + +nsresult nsMsgSearchTerm::MatchSize(uint32_t sizeToMatch, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv = NS_OK; + bool result = false; + // We reduce the sizeToMatch rather than supplied size + // as then we can do an exact match on the displayed value + // which will be less confusing to the user. + uint32_t sizeToMatchKB = sizeToMatch; + + if (sizeToMatchKB < 1024) sizeToMatchKB = 1024; + + sizeToMatchKB /= 1024; + + switch (m_operator) { + case nsMsgSearchOp::IsGreaterThan: + if (sizeToMatchKB > m_value.u.size) result = true; + break; + case nsMsgSearchOp::IsLessThan: + if (sizeToMatchKB < m_value.u.size) result = true; + break; + case nsMsgSearchOp::Is: + if (sizeToMatchKB == m_value.u.size) result = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for size to match"); + } + *pResult = result; + return rv; +} + +nsresult nsMsgSearchTerm::MatchJunkStatus(const char* aJunkScore, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + if (m_operator == nsMsgSearchOp::IsEmpty) { + *pResult = !(aJunkScore && *aJunkScore); + return NS_OK; + } + if (m_operator == nsMsgSearchOp::IsntEmpty) { + *pResult = (aJunkScore && *aJunkScore); + return NS_OK; + } + + nsMsgJunkStatus junkStatus; + if (aJunkScore && *aJunkScore) { + junkStatus = (atoi(aJunkScore) == nsIJunkMailPlugin::IS_SPAM_SCORE) + ? nsIJunkMailPlugin::JUNK + : nsIJunkMailPlugin::GOOD; + } else { + // the in UI, we only show "junk" or "not junk" + // unknown, or nsIJunkMailPlugin::UNCLASSIFIED is shown as not junk + // so for the search to work as expected, treat unknown as not junk + junkStatus = nsIJunkMailPlugin::GOOD; + } + + nsresult rv = NS_OK; + bool matches = (junkStatus == m_value.u.junkStatus); + + switch (m_operator) { + case nsMsgSearchOp::Is: + break; + case nsMsgSearchOp::Isnt: + matches = !matches; + break; + default: + rv = NS_ERROR_FAILURE; + matches = false; + NS_ERROR("invalid compare op for junk status"); + } + + *pResult = matches; + return rv; +} + +nsresult nsMsgSearchTerm::MatchJunkScoreOrigin(const char* aJunkScoreOrigin, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + bool matches = false; + nsresult rv = NS_OK; + + switch (m_operator) { + case nsMsgSearchOp::Is: + matches = aJunkScoreOrigin && m_value.utf8String.Equals(aJunkScoreOrigin); + break; + case nsMsgSearchOp::Isnt: + matches = + !aJunkScoreOrigin || !m_value.utf8String.Equals(aJunkScoreOrigin); + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for junk score origin"); + } + + *pResult = matches; + return rv; +} + +nsresult nsMsgSearchTerm::MatchJunkPercent(uint32_t aJunkPercent, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv = NS_OK; + bool result = false; + switch (m_operator) { + case nsMsgSearchOp::IsGreaterThan: + if (aJunkPercent > m_value.u.junkPercent) result = true; + break; + case nsMsgSearchOp::IsLessThan: + if (aJunkPercent < m_value.u.junkPercent) result = true; + break; + case nsMsgSearchOp::Is: + if (aJunkPercent == m_value.u.junkPercent) result = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for junk percent"); + } + *pResult = result; + return rv; +} + +// MatchStatus () is not only used for nsMsgMessageFlags but also for +// nsMsgFolderFlags (both being 'unsigned long') +nsresult nsMsgSearchTerm::MatchStatus(uint32_t statusToMatch, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv = NS_OK; + bool matches = (statusToMatch & m_value.u.msgStatus); + + // nsMsgSearchOp::Is and nsMsgSearchOp::Isnt are intentionally used as + // Contains and DoesntContain respectively, for legacy reasons. + switch (m_operator) { + case nsMsgSearchOp::Is: + break; + case nsMsgSearchOp::Isnt: + matches = !matches; + break; + default: + rv = NS_ERROR_FAILURE; + matches = false; + NS_ERROR("invalid compare op for msg status"); + } + + *pResult = matches; + return rv; +} + +/* + * MatchKeyword Logic table (*pResult: + is true, - is false) + * + * # Valid Tokens IsEmpty IsntEmpty Contains DoesntContain Is Isnt + * 0 + - - + - + + * Term found? N Y N Y N Y N Y + * 1 - + - + + - - + + - + * >1 - + - + + - - - + + + */ +// look up nsMsgSearchTerm::m_value in space-delimited keywordList +nsresult nsMsgSearchTerm::MatchKeyword(const nsACString& keywordList, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + bool matches = false; + + // special-case empty for performance reasons + if (keywordList.IsEmpty()) { + *pResult = m_operator != nsMsgSearchOp::Contains && + m_operator != nsMsgSearchOp::Is && + m_operator != nsMsgSearchOp::IsntEmpty; + return NS_OK; + } + + // check if we can skip expensive valid keywordList test + if (m_operator == nsMsgSearchOp::DoesntContain || + m_operator == nsMsgSearchOp::Contains) { + nsCString keywordString(keywordList); + const uint32_t kKeywordLen = m_value.utf8String.Length(); + const char* matchStart = + PL_strstr(keywordString.get(), m_value.utf8String.get()); + while (matchStart) { + // For a real match, matchStart must be the start of the keywordList or + // preceded by a space and matchEnd must point to a \0 or space. + const char* matchEnd = matchStart + kKeywordLen; + if ((matchStart == keywordString.get() || matchStart[-1] == ' ') && + (!*matchEnd || *matchEnd == ' ')) { + // found the keyword + *pResult = m_operator == nsMsgSearchOp::Contains; + return NS_OK; + } + // no match yet, so search on + matchStart = PL_strstr(matchEnd, m_value.utf8String.get()); + } + // keyword not found + *pResult = m_operator == nsMsgSearchOp::DoesntContain; + return NS_OK; + } + + // Only accept valid keys in tokens. + nsresult rv = NS_OK; + nsTArray<nsCString> keywordArray; + ParseString(keywordList, ' ', keywordArray); + nsCOMPtr<nsIMsgTagService> tagService( + do_GetService("@mozilla.org/messenger/tagservice;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Loop through tokens in keywords + uint32_t count = keywordArray.Length(); + for (uint32_t i = 0; i < count; i++) { + // is this token a valid tag? Otherwise ignore it + bool isValid; + rv = tagService->IsValidKey(keywordArray[i], &isValid); + NS_ENSURE_SUCCESS(rv, rv); + + if (isValid) { + // IsEmpty fails on any valid token + if (m_operator == nsMsgSearchOp::IsEmpty) { + *pResult = false; + return rv; + } + + // IsntEmpty succeeds on any valid token + if (m_operator == nsMsgSearchOp::IsntEmpty) { + *pResult = true; + return rv; + } + + // Does this valid tag key match our search term? + matches = keywordArray[i].Equals(m_value.utf8String); + + // Is or Isn't partly determined on a single unmatched token + if (!matches) { + if (m_operator == nsMsgSearchOp::Is) { + *pResult = false; + return rv; + } + if (m_operator == nsMsgSearchOp::Isnt) { + *pResult = true; + return rv; + } + } + } + } + + if (m_operator == nsMsgSearchOp::Is) { + *pResult = matches; + return NS_OK; + } + + if (m_operator == nsMsgSearchOp::Isnt) { + *pResult = !matches; + return NS_OK; + } + + if (m_operator == nsMsgSearchOp::IsEmpty) { + *pResult = true; + return NS_OK; + } + + if (m_operator == nsMsgSearchOp::IsntEmpty) { + *pResult = false; + return NS_OK; + } + + // no valid match operator found + *pResult = false; + NS_ERROR("invalid compare op for msg status"); + return NS_ERROR_FAILURE; +} + +nsresult nsMsgSearchTerm::MatchPriority(nsMsgPriorityValue priorityToMatch, + bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv = NS_OK; + bool result = false; + + // Use this ugly little hack to get around the fact that enums don't have + // integer compare operators + int p1 = (priorityToMatch == nsMsgPriority::none) ? (int)nsMsgPriority::normal + : (int)priorityToMatch; + int p2 = (int)m_value.u.priority; + + switch (m_operator) { + case nsMsgSearchOp::IsHigherThan: + if (p1 > p2) result = true; + break; + case nsMsgSearchOp::IsLowerThan: + if (p1 < p2) result = true; + break; + case nsMsgSearchOp::Is: + if (p1 == p2) result = true; + break; + case nsMsgSearchOp::Isnt: + if (p1 != p2) result = true; + break; + default: + rv = NS_ERROR_FAILURE; + NS_ERROR("invalid compare op for priority"); + } + *pResult = result; + return rv; +} + +// match a custom search term +NS_IMETHODIMP nsMsgSearchTerm::MatchCustom(nsIMsgDBHdr* aHdr, bool* pResult) { + NS_ENSURE_ARG_POINTER(pResult); + + nsresult rv; + nsCOMPtr<nsIMsgFilterService> filterService = + do_GetService("@mozilla.org/messenger/services/filters;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgSearchCustomTerm> customTerm; + rv = filterService->GetCustomTerm(m_customId, getter_AddRefs(customTerm)); + NS_ENSURE_SUCCESS(rv, rv); + + if (customTerm) + return customTerm->Match(aHdr, m_value.utf8String, m_operator, pResult); + *pResult = false; // default to no match if term is missing + return NS_ERROR_FAILURE; // missing custom term +} + +// set the id of a custom search term +NS_IMETHODIMP nsMsgSearchTerm::SetCustomId(const nsACString& aId) { + m_customId = aId; + return NS_OK; +} + +// get the id of a custom search term +NS_IMETHODIMP nsMsgSearchTerm::GetCustomId(nsACString& aResult) { + aResult = m_customId; + return NS_OK; +} + +NS_IMPL_GETSET(nsMsgSearchTerm, Attrib, nsMsgSearchAttribValue, m_attribute) +NS_IMPL_GETSET(nsMsgSearchTerm, Op, nsMsgSearchOpValue, m_operator) +NS_IMPL_GETSET(nsMsgSearchTerm, MatchAll, bool, m_matchAll) + +NS_IMETHODIMP +nsMsgSearchTerm::GetValue(nsIMsgSearchValue** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_ADDREF(*aResult = new nsMsgSearchValueImpl(&m_value)); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::SetValue(nsIMsgSearchValue* aValue) { + nsMsgResultElement::AssignValues(aValue, &m_value); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::GetBooleanAnd(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = (m_booleanOp == nsMsgSearchBooleanOp::BooleanAND); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::SetBooleanAnd(bool aValue) { + if (aValue) + m_booleanOp = nsMsgSearchBooleanOperator(nsMsgSearchBooleanOp::BooleanAND); + else + m_booleanOp = nsMsgSearchBooleanOperator(nsMsgSearchBooleanOp::BooleanOR); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::GetArbitraryHeader(nsACString& aResult) { + aResult = m_arbitraryHeader; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::SetArbitraryHeader(const nsACString& aValue) { + m_arbitraryHeader = aValue; + ToLowerCaseExceptSpecials(m_arbitraryHeader); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::GetHdrProperty(nsACString& aResult) { + aResult = m_hdrProperty; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchTerm::SetHdrProperty(const nsACString& aValue) { + m_hdrProperty = aValue; + ToLowerCaseExceptSpecials(m_hdrProperty); + return NS_OK; +} + +NS_IMPL_GETSET(nsMsgSearchTerm, BeginsGrouping, bool, mBeginsGrouping) +NS_IMPL_GETSET(nsMsgSearchTerm, EndsGrouping, bool, mEndsGrouping) + +// +// Certain possible standard values of a message database row also sometimes +// appear as header values. To prevent a naming collision, we use all +// lower case for the standard headers, and first capital when those +// same strings are requested as arbitrary headers. This routine is used +// when setting arbitrary headers. +// +void nsMsgSearchTerm::ToLowerCaseExceptSpecials(nsACString& aValue) { + if (aValue.LowerCaseEqualsLiteral("sender")) + aValue.AssignLiteral("Sender"); + else if (aValue.LowerCaseEqualsLiteral("date")) + aValue.AssignLiteral("Date"); + else if (aValue.LowerCaseEqualsLiteral("status")) + aValue.AssignLiteral("Status"); + else + ToLowerCase(aValue); +} + +//----------------------------------------------------------------------------- +// nsMsgSearchScopeTerm implementation +//----------------------------------------------------------------------------- +nsMsgSearchScopeTerm::nsMsgSearchScopeTerm(nsIMsgSearchSession* session, + nsMsgSearchScopeValue attribute, + nsIMsgFolder* folder) { + m_attribute = attribute; + m_folder = folder; + m_searchServer = true; + m_searchSession = do_GetWeakReference(session); +} + +nsMsgSearchScopeTerm::nsMsgSearchScopeTerm() { m_searchServer = true; } + +nsMsgSearchScopeTerm::~nsMsgSearchScopeTerm() { + if (m_inputStream) m_inputStream->Close(); + m_inputStream = nullptr; +} + +NS_IMPL_ISUPPORTS(nsMsgSearchScopeTerm, nsIMsgSearchScopeTerm) + +NS_IMETHODIMP +nsMsgSearchScopeTerm::GetFolder(nsIMsgFolder** aResult) { + NS_IF_ADDREF(*aResult = m_folder); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchScopeTerm::GetSearchSession(nsIMsgSearchSession** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + nsCOMPtr<nsIMsgSearchSession> searchSession = + do_QueryReferent(m_searchSession); + searchSession.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchScopeTerm::GetInputStream(nsIMsgDBHdr* aMsgHdr, + nsIInputStream** aInputStream) { + NS_ENSURE_ARG_POINTER(aInputStream); + NS_ENSURE_ARG_POINTER(aMsgHdr); + NS_ENSURE_TRUE(m_folder, NS_ERROR_NULL_POINTER); + nsresult rv = + m_folder->GetMsgInputStream(aMsgHdr, getter_AddRefs(m_inputStream)); + NS_ENSURE_SUCCESS(rv, rv); + NS_IF_ADDREF(*aInputStream = m_inputStream); + return rv; +} + +NS_IMETHODIMP nsMsgSearchScopeTerm::CloseInputStream() { + if (m_inputStream) { + m_inputStream->Close(); + m_inputStream = nullptr; + } + return NS_OK; +} + +nsresult nsMsgSearchScopeTerm::TimeSlice(bool* aDone) { + return m_adapter->Search(aDone); +} + +nsresult nsMsgSearchScopeTerm::InitializeAdapter( + nsTArray<RefPtr<nsIMsgSearchTerm>> const& termList) { + if (m_adapter) return NS_OK; + + nsresult rv = NS_OK; + + switch (m_attribute) { + case nsMsgSearchScope::onlineMail: + m_adapter = new nsMsgSearchOnlineMail(this, termList); + break; + case nsMsgSearchScope::offlineMail: + case nsMsgSearchScope::onlineManual: + m_adapter = new nsMsgSearchOfflineMail(this, termList); + break; + case nsMsgSearchScope::newsEx: + NS_ASSERTION(false, "not supporting newsEx yet"); + break; + case nsMsgSearchScope::news: + m_adapter = new nsMsgSearchNews(this, termList); + break; + case nsMsgSearchScope::allSearchableGroups: + NS_ASSERTION(false, "not supporting allSearchableGroups yet"); + break; + case nsMsgSearchScope::LDAP: + NS_ASSERTION(false, "not supporting LDAP yet"); + break; + case nsMsgSearchScope::localNews: + case nsMsgSearchScope::localNewsJunk: + case nsMsgSearchScope::localNewsBody: + case nsMsgSearchScope::localNewsJunkBody: + m_adapter = new nsMsgSearchOfflineNews(this, termList); + break; + default: + NS_ASSERTION(false, "invalid scope"); + rv = NS_ERROR_FAILURE; + } + + if (m_adapter) rv = m_adapter->ValidateTerms(); + + return rv; +} + +char* nsMsgSearchScopeTerm::GetStatusBarName() { return nullptr; } + +//----------------------------------------------------------------------------- +// nsMsgResultElement implementation +//----------------------------------------------------------------------------- + +nsMsgResultElement::nsMsgResultElement(nsIMsgSearchAdapter* adapter) { + m_adapter = adapter; +} + +nsMsgResultElement::~nsMsgResultElement() {} + +nsresult nsMsgResultElement::AddValue(nsIMsgSearchValue* value) { + m_valueList.AppendElement(value); + return NS_OK; +} + +nsresult nsMsgResultElement::AddValue(nsMsgSearchValue* value) { + nsMsgSearchValueImpl* valueImpl = new nsMsgSearchValueImpl(value); + delete value; // we keep the nsIMsgSearchValue, not + // the nsMsgSearchValue + return AddValue(valueImpl); +} + +nsresult nsMsgResultElement::AssignValues(nsIMsgSearchValue* src, + nsMsgSearchValue* dst) { + NS_ENSURE_ARG_POINTER(src); + NS_ENSURE_ARG_POINTER(dst); + // Yes, this could be an operator overload, but nsMsgSearchValue is totally + // public, so I'd have to define a derived class with nothing by operator=, + // and that seems like a bit much + nsresult rv = NS_OK; + src->GetAttrib(&dst->attribute); + switch (dst->attribute) { + case nsMsgSearchAttrib::Priority: + rv = src->GetPriority(&dst->u.priority); + break; + case nsMsgSearchAttrib::Date: + rv = src->GetDate(&dst->u.date); + break; + case nsMsgSearchAttrib::HasAttachmentStatus: + case nsMsgSearchAttrib::MsgStatus: + case nsMsgSearchAttrib::FolderFlag: + case nsMsgSearchAttrib::Uint32HdrProperty: + rv = src->GetStatus(&dst->u.msgStatus); + break; + case nsMsgSearchAttrib::MessageKey: + rv = src->GetMsgKey(&dst->u.key); + break; + case nsMsgSearchAttrib::AgeInDays: + rv = src->GetAge(&dst->u.age); + break; + case nsMsgSearchAttrib::JunkStatus: + rv = src->GetJunkStatus(&dst->u.junkStatus); + break; + case nsMsgSearchAttrib::JunkPercent: + rv = src->GetJunkPercent(&dst->u.junkPercent); + break; + case nsMsgSearchAttrib::Size: + rv = src->GetSize(&dst->u.size); + break; + default: + if (dst->attribute < nsMsgSearchAttrib::kNumMsgSearchAttributes) { + NS_ASSERTION(IS_STRING_ATTRIBUTE(dst->attribute), + "assigning non-string result"); + nsString unicodeString; + rv = src->GetStr(unicodeString); + CopyUTF16toUTF8(unicodeString, dst->utf8String); + dst->utf16String = unicodeString; + } else + rv = NS_ERROR_INVALID_ARG; + } + return rv; +} + +nsresult nsMsgResultElement::GetValue(nsMsgSearchAttribValue attrib, + nsMsgSearchValue** outValue) const { + nsresult rv = NS_OK; + *outValue = NULL; + + for (uint32_t i = 0; i < m_valueList.Length() && NS_FAILED(rv); i++) { + nsMsgSearchAttribValue valueAttribute; + m_valueList[i]->GetAttrib(&valueAttribute); + if (attrib == valueAttribute) { + *outValue = new nsMsgSearchValue; + if (*outValue) { + rv = AssignValues(m_valueList[i], *outValue); + // Now this is really strange! What is this assignment doing here? + rv = NS_OK; + } else + rv = NS_ERROR_OUT_OF_MEMORY; + } + } + return rv; +} + +nsresult nsMsgResultElement::GetPrettyName(nsMsgSearchValue** value) { + return GetValue(nsMsgSearchAttrib::Location, value); +} + +nsresult nsMsgResultElement::Open(void* window) { + return NS_ERROR_NULL_POINTER; +} |