/* -*- 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;
}
