NS_IMETHODIMP nsImapMailFolder::ApplyFilterHit()

in mailnews/imap/src/nsImapMailFolder.cpp [3265:3638]


NS_IMETHODIMP nsImapMailFolder::ApplyFilterHit(nsIMsgFilter* filter,
                                               nsIMsgWindow* msgWindow,
                                               bool* applyMore) {
  //
  //  This routine is called indirectly from ApplyFiltersToHdr in two
  //  circumstances, controlled by m_filterListRequiresBody:
  //
  //  If false, after headers are parsed in NormalEndHeaderParseStream.
  //  If true, after the message body is downloaded in NormalEndMsgWriteStream.
  //
  //  In NormalEndHeaderParseStream, the message has not been added to the
  //  database, and it is important that database notifications and count
  //  updates do not occur. In NormalEndMsgWriteStream, the message has been
  //  added to the database, and database notifications and count updates
  //  should be performed.
  //

  NS_ENSURE_ARG_POINTER(filter);
  NS_ENSURE_ARG_POINTER(applyMore);

  nsresult rv = NS_OK;

  nsCOMPtr<nsIMsgDBHdr> msgHdr;
  if (m_filterListRequiresBody)
    GetMessageHeader(m_curMsgUid, getter_AddRefs(msgHdr));
  else if (m_msgParser)
    m_msgParser->GetNewMsgHdr(getter_AddRefs(msgHdr));
  NS_ENSURE_TRUE(msgHdr,
                 NS_ERROR_NULL_POINTER);  // fatal error, cannot apply filters

  bool deleteToTrash = DeleteIsMoveToTrash();

  nsTArray<RefPtr<nsIMsgRuleAction>> filterActionList;
  rv = filter->GetSortedActionList(filterActionList);
  NS_ENSURE_SUCCESS(rv, rv);

  uint32_t numActions = filterActionList.Length();

  nsCString msgId;
  msgHdr->GetMessageId(msgId);
  nsMsgKey msgKey;
  msgHdr->GetMessageKey(&msgKey);
  MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
          ("(Imap) Applying %" PRIu32
           " filter actions on message with key %" PRIu32,
           numActions, msgKeyToInt(msgKey)));
  MOZ_LOG(FILTERLOGMODULE, LogLevel::Debug,
          ("(Imap) Message ID: %s", msgId.get()));

  bool loggingEnabled = false;
  if (m_filterList && numActions)
    (void)m_filterList->GetLoggingEnabled(&loggingEnabled);

  bool msgIsNew = true;

  rv = GetDatabase();
  NS_ENSURE_SUCCESS(rv, rv);

  nsresult finalResult = NS_OK;  // result of all actions
  for (uint32_t actionIndex = 0; actionIndex < numActions; actionIndex++) {
    nsCOMPtr<nsIMsgRuleAction> filterAction(filterActionList[actionIndex]);
    if (!filterAction) {
      MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning,
              ("(Imap) Filter action at index %" PRIu32 " invalid, skipping",
               actionIndex));
      continue;
    }

    rv = NS_OK;  // result of the current action
    nsMsgRuleActionType actionType;
    if (NS_SUCCEEDED(filterAction->GetType(&actionType))) {
      MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
              ("(Imap) Running filter action at index %" PRIu32
               ", action type = %i",
               actionIndex, actionType));
      if (loggingEnabled) (void)filter->LogRuleHit(filterAction, msgHdr);

      nsCString actionTargetFolderUri;
      if (actionType == nsMsgFilterAction::MoveToFolder ||
          actionType == nsMsgFilterAction::CopyToFolder) {
        rv = filterAction->GetTargetFolderUri(actionTargetFolderUri);
        if (NS_FAILED(rv) || actionTargetFolderUri.IsEmpty()) {
          // clang-format off
          MOZ_LOG(FILTERLOGMODULE, LogLevel::Warning,
                  ("(Imap) Target URI for Copy/Move action is empty, skipping"));
          // clang-format on
          NS_ASSERTION(false, "actionTargetFolderUri is empty");
          continue;
        }
      }

      uint32_t msgFlags;
      msgHdr->GetFlags(&msgFlags);
      bool isRead = (msgFlags & nsMsgMessageFlags::Read);

      switch (actionType) {
        case nsMsgFilterAction::Delete: {
          if (deleteToTrash) {
            // set value to trash folder
            nsCOMPtr<nsIMsgFolder> mailTrash;
            rv = GetTrashFolder(getter_AddRefs(mailTrash));
            if (NS_SUCCEEDED(rv) && mailTrash) {
              rv = mailTrash->GetURI(actionTargetFolderUri);
              if (NS_FAILED(rv)) break;
            }
            // msgHdr->OrFlags(nsMsgMessageFlags::Read, &newFlags);  // mark
            // read in trash.
          } else {
            mDatabase->MarkRead(msgKey, true, nullptr);
            mDatabase->MarkImapDeleted(msgKey, true, nullptr);
            rv = StoreImapFlags(kImapMsgSeenFlag | kImapMsgDeletedFlag, true,
                                {msgKey}, nullptr);
            if (NS_FAILED(rv)) break;
            // this will prevent us from adding the header to the db.
            m_msgMovedByFilter = true;
          }
          msgIsNew = false;
        }
          // note that delete falls through to move.
          [[fallthrough]];
        case nsMsgFilterAction::MoveToFolder: {
          // if moving to a different file, do it.
          nsCString uri;
          rv = GetURI(uri);
          if (NS_FAILED(rv)) break;

          if (!actionTargetFolderUri.Equals(uri)) {
            msgHdr->GetFlags(&msgFlags);
            if (msgFlags & nsMsgMessageFlags::MDNReportNeeded && !isRead) {
              mDatabase->MarkMDNNeeded(msgKey, false, nullptr);
              mDatabase->MarkMDNSent(msgKey, true, nullptr);
            }
            nsresult rv = MoveIncorporatedMessage(
                msgHdr, mDatabase, actionTargetFolderUri, filter, msgWindow);
            if (NS_SUCCEEDED(rv)) {
              m_msgMovedByFilter = true;
            } else {
              if (loggingEnabled) {
                (void)filter->LogRuleHitFail(filterAction, msgHdr, rv,
                                             "filterFailureMoveFailed"_ns);
              }
            }
          }
          // don't apply any more filters, even if it was a move to the same
          // folder
          *applyMore = false;
        } break;
        case nsMsgFilterAction::CopyToFolder: {
          nsCString uri;
          rv = GetURI(uri);
          if (NS_FAILED(rv)) break;

          if (!actionTargetFolderUri.Equals(uri)) {
            // XXXshaver I'm not actually 100% what the right semantics are for
            // MDNs and copied messages, but I suspect deep down inside that
            // we probably want to suppress them only on the copies.
            msgHdr->GetFlags(&msgFlags);
            if (msgFlags & nsMsgMessageFlags::MDNReportNeeded && !isRead) {
              mDatabase->MarkMDNNeeded(msgKey, false, nullptr);
              mDatabase->MarkMDNSent(msgKey, true, nullptr);
            }

            nsCOMPtr<nsIMsgFolder> dstFolder;
            rv = GetExistingFolder(actionTargetFolderUri,
                                   getter_AddRefs(dstFolder));
            if (NS_FAILED(rv)) break;

            nsCOMPtr<nsIMsgCopyService> copyService = do_GetService(
                "@mozilla.org/messenger/messagecopyservice;1", &rv);
            if (NS_FAILED(rv)) break;
            rv = copyService->CopyMessages(this, {&*msgHdr}, dstFolder, false,
                                           nullptr, msgWindow, false);
            if (NS_FAILED(rv)) {
              if (loggingEnabled) {
                (void)filter->LogRuleHitFail(filterAction, msgHdr, rv,
                                             "filterFailureCopyFailed"_ns);
              }
            }
          }
        } break;
        case nsMsgFilterAction::MarkRead: {
          mDatabase->MarkRead(msgKey, true, nullptr);
          rv = StoreImapFlags(kImapMsgSeenFlag, true, {msgKey}, nullptr);
          msgIsNew = false;
        } break;
        case nsMsgFilterAction::MarkUnread: {
          mDatabase->MarkRead(msgKey, false, nullptr);
          rv = StoreImapFlags(kImapMsgSeenFlag, false, {msgKey}, nullptr);
          msgIsNew = true;
        } break;
        case nsMsgFilterAction::MarkFlagged: {
          mDatabase->MarkMarked(msgKey, true, nullptr);
          rv = StoreImapFlags(kImapMsgFlaggedFlag, true, {msgKey}, nullptr);
        } break;
        case nsMsgFilterAction::KillThread:
        case nsMsgFilterAction::WatchThread: {
          nsCOMPtr<nsIMsgThread> msgThread;
          nsMsgKey threadKey;
          mDatabase->GetThreadContainingMsgHdr(msgHdr,
                                               getter_AddRefs(msgThread));
          if (msgThread) {
            msgThread->GetThreadKey(&threadKey);
            if (actionType == nsMsgFilterAction::KillThread)
              rv = mDatabase->MarkThreadIgnored(msgThread, threadKey, true,
                                                nullptr);
            else
              rv = mDatabase->MarkThreadWatched(msgThread, threadKey, true,
                                                nullptr);
          } else {
            if (actionType == nsMsgFilterAction::KillThread)
              rv = msgHdr->SetUint32Property("ProtoThreadFlags",
                                             nsMsgMessageFlags::Ignored);
            else
              rv = msgHdr->SetUint32Property("ProtoThreadFlags",
                                             nsMsgMessageFlags::Watched);
          }
          if (actionType == nsMsgFilterAction::KillThread) {
            mDatabase->MarkRead(msgKey, true, nullptr);
            rv = StoreImapFlags(kImapMsgSeenFlag, true, {msgKey}, nullptr);
            msgIsNew = false;
          }
        } break;
        case nsMsgFilterAction::KillSubthread: {
          mDatabase->MarkKilled(msgKey, true, nullptr);
          mDatabase->MarkRead(msgKey, true, nullptr);
          rv = StoreImapFlags(kImapMsgSeenFlag, true, {msgKey}, nullptr);
          msgIsNew = false;
        } break;
        case nsMsgFilterAction::ChangePriority: {
          nsMsgPriorityValue filterPriority;  // a int32_t
          filterAction->GetPriority(&filterPriority);
          rv = mDatabase->SetUint32PropertyByHdr(
              msgHdr, "priority", static_cast<uint32_t>(filterPriority));
        } break;
        case nsMsgFilterAction::AddTag: {
          nsCString keyword;
          filterAction->GetStrValue(keyword);
          rv = AddKeywordsToMessages({&*msgHdr}, keyword);
        } break;
        case nsMsgFilterAction::JunkScore: {
          nsAutoCString junkScoreStr;
          int32_t junkScore;
          filterAction->GetJunkScore(&junkScore);
          SetJunkScoreForMessage(msgHdr, junkScore, "filter"_ns, -1);

          // If score is available, set up to store junk status on server.
          if (junkScore == nsIJunkMailPlugin::IS_SPAM_SCORE ||
              junkScore == nsIJunkMailPlugin::IS_HAM_SCORE) {
            nsTArray<nsMsgKey>* keysToClassify = m_moveCoalescer->GetKeyBucket(
                (junkScore == nsIJunkMailPlugin::IS_SPAM_SCORE) ? 0 : 1);
            NS_ASSERTION(keysToClassify, "error getting key bucket");
            if (keysToClassify) keysToClassify->AppendElement(msgKey);
            if (msgIsNew && junkScore == nsIJunkMailPlugin::IS_SPAM_SCORE) {
              msgIsNew = false;
              mDatabase->MarkNotNew(msgKey, nullptr);
              // nsMsgDBFolder::SendFlagNotifications by the call to
              // SetBiffState(nsMsgBiffState_NoMail) will reset numNewMessages
              // only if the message is also read and database notifications
              // are active, but we are not going to mark it read in this
              // action, preferring to leave the choice to the user.
              // So correct numNewMessages.
              if (m_filterListRequiresBody) {
                msgHdr->GetFlags(&msgFlags);
                if (!(msgFlags & nsMsgMessageFlags::Read)) {
                  int32_t numNewMessages;
                  GetNumNewMessages(false, &numNewMessages);
                  SetNumNewMessages(--numNewMessages);
                  SetHasNewMessages(numNewMessages != 0);
                }
              }
            }
          }
        } break;
        case nsMsgFilterAction::Forward: {
          nsCString forwardTo;
          filterAction->GetStrValue(forwardTo);
          nsCOMPtr<nsIMsgIncomingServer> server;
          rv = GetServer(getter_AddRefs(server));
          if (NS_FAILED(rv)) break;
          if (!forwardTo.IsEmpty()) {
            nsCOMPtr<nsIMsgComposeService> compService =
                do_GetService("@mozilla.org/messengercompose;1", &rv);
            if (NS_FAILED(rv)) break;
            rv = compService->ForwardMessage(
                NS_ConvertUTF8toUTF16(forwardTo), msgHdr, msgWindow, server,
                nsIMsgComposeService::kForwardAsDefault);
          }
        } break;

        case nsMsgFilterAction::Reply: {
          nsCString replyTemplateUri;
          filterAction->GetStrValue(replyTemplateUri);
          nsCOMPtr<nsIMsgIncomingServer> server;
          rv = GetServer(getter_AddRefs(server));
          if (NS_FAILED(rv)) break;
          if (!replyTemplateUri.IsEmpty()) {
            nsCOMPtr<nsIMsgComposeService> compService =
                do_GetService("@mozilla.org/messengercompose;1", &rv);
            if (NS_SUCCEEDED(rv) && compService) {
              rv = compService->ReplyWithTemplate(msgHdr, replyTemplateUri,
                                                  msgWindow, server);
              if (NS_FAILED(rv)) {
                NS_WARNING("ReplyWithTemplate failed");
                if (rv == NS_ERROR_ABORT) {
                  (void)filter->LogRuleHitFail(
                      filterAction, msgHdr, rv,
                      "filterFailureSendingReplyAborted"_ns);
                } else {
                  (void)filter->LogRuleHitFail(
                      filterAction, msgHdr, rv,
                      "filterFailureSendingReplyError"_ns);
                }
              }
            }
          }
        } break;

        case nsMsgFilterAction::StopExecution: {
          // don't apply any more filters
          *applyMore = false;
          rv = NS_OK;
        } break;

        case nsMsgFilterAction::Custom: {
          nsCOMPtr<nsIMsgFilterCustomAction> customAction;
          rv = filterAction->GetCustomAction(getter_AddRefs(customAction));
          if (NS_FAILED(rv)) break;

          nsAutoCString value;
          rv = filterAction->GetStrValue(value);
          if (NS_FAILED(rv)) break;

          rv = customAction->ApplyAction({&*msgHdr}, value, nullptr,
                                         nsMsgFilterType::InboxRule, msgWindow);
          // allow custom action to affect new
          msgHdr->GetFlags(&msgFlags);
          if (!(msgFlags & nsMsgMessageFlags::New)) msgIsNew = false;
        } break;

        default:
          NS_ERROR("unexpected filter action");
          rv = NS_ERROR_UNEXPECTED;
          break;
      }
    }
    if (NS_FAILED(rv)) {
      finalResult = rv;
      MOZ_LOG(FILTERLOGMODULE, LogLevel::Error,
              ("(Imap) Action execution failed with error: %" PRIx32,
               static_cast<uint32_t>(rv)));
      if (loggingEnabled) {
        (void)filter->LogRuleHitFail(filterAction, msgHdr, rv,
                                     "filterFailureAction"_ns);
      }
    } else {
      MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
              ("(Imap) Action execution succeeded"));
    }
  }
  if (!msgIsNew) {
    int32_t numNewMessages;
    GetNumNewMessages(false, &numNewMessages);
    // When database notifications are active, new counts will be reset
    // to zero in nsMsgDBFolder::SendFlagNotifications by the call to
    // SetBiffState(nsMsgBiffState_NoMail), so don't repeat them here.
    if (!m_filterListRequiresBody) SetNumNewMessages(--numNewMessages);
    if (mDatabase) mDatabase->MarkNotNew(msgKey, nullptr);
    MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
            ("(Imap) Message will not be marked new"));
  }
  MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
          ("(Imap) Finished executing actions"));
  return finalResult;
}