diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/base/src/nsMsgDBView.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mailnews/base/src/nsMsgDBView.cpp')
-rw-r--r-- | comm/mailnews/base/src/nsMsgDBView.cpp | 7411 |
1 files changed, 7411 insertions, 0 deletions
diff --git a/comm/mailnews/base/src/nsMsgDBView.cpp b/comm/mailnews/base/src/nsMsgDBView.cpp new file mode 100644 index 0000000000..6532b52e0e --- /dev/null +++ b/comm/mailnews/base/src/nsMsgDBView.cpp @@ -0,0 +1,7411 @@ +/* -*- 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 <algorithm> +#include "msgCore.h" +#include "prmem.h" +#include "nsArrayUtils.h" +#include "nsIMsgCustomColumnHandler.h" +#include "nsMsgDBView.h" +#include "nsISupports.h" +#include "nsIMsgFolder.h" +#include "nsIDBFolderInfo.h" +#include "nsIMsgDatabase.h" +#include "nsIMsgFolder.h" +#include "MailNewsTypes2.h" +#include "nsIMsgImapMailFolder.h" +#include "nsImapCore.h" +#include "nsMsgFolderFlags.h" +#include "nsIMsgLocalMailFolder.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIPrefLocalizedString.h" +#include "nsIMsgSearchSession.h" +#include "nsIMsgCopyService.h" +#include "nsISpamSettings.h" +#include "nsIMsgAccountManager.h" +#include "nsTreeColumns.h" +#include "nsTextFormatter.h" +#include "nsIMimeConverter.h" +#include "nsMsgMessageFlags.h" +#include "nsIPrompt.h" +#include "nsIWindowWatcher.h" +#include "nsIMsgFolderNotificationService.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsMemory.h" +#include "nsIAbManager.h" +#include "nsIAbDirectory.h" +#include "nsIAbCard.h" +#include "mozilla/Components.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/mailnews/MimeHeaderParser.h" +#include "nsTArray.h" +#include "mozilla/intl/OSPreferences.h" +#include "mozilla/intl/LocaleService.h" +#include "mozilla/intl/AppDateTimeFormat.h" + +using namespace mozilla::mailnews; + +nsString nsMsgDBView::kHighestPriorityString; +nsString nsMsgDBView::kHighPriorityString; +nsString nsMsgDBView::kLowestPriorityString; +nsString nsMsgDBView::kLowPriorityString; +nsString nsMsgDBView::kNormalPriorityString; + +nsString nsMsgDBView::kReadString; +nsString nsMsgDBView::kRepliedString; +nsString nsMsgDBView::kForwardedString; +nsString nsMsgDBView::kRedirectedString; +nsString nsMsgDBView::kNewString; + +nsString nsMsgDBView::kTodayString; +nsString nsMsgDBView::kYesterdayString; +nsString nsMsgDBView::kLastWeekString; +nsString nsMsgDBView::kTwoWeeksAgoString; +nsString nsMsgDBView::kOldMailString; +nsString nsMsgDBView::kFutureDateString; + +bool nsMsgDBView::m_dateFormatsInitialized = false; +nsDateFormatSelectorComm nsMsgDBView::m_dateFormatDefault = kDateFormatShort; +nsDateFormatSelectorComm nsMsgDBView::m_dateFormatThisWeek = kDateFormatShort; +nsDateFormatSelectorComm nsMsgDBView::m_dateFormatToday = kDateFormatNone; + +nsString nsMsgDBView::m_connectorPattern; +nsCOMPtr<nsIStringBundle> nsMsgDBView::mMessengerStringBundle; + +static const uint32_t kMaxNumSortColumns = 2; + +static void GetCachedName(const nsCString& unparsedString, + int32_t displayVersion, nsACString& cachedName); + +static void UpdateCachedName(nsIMsgDBHdr* aHdr, const char* header_field, + const nsAString& newName); + +// viewSortInfo is context data passed into the sort comparison functions - +// FnSortIdUint32 for comparing numeric fields, FnSortIdKey for everything +// else. If a comparison function finds two elements with equal primary +// ordering, it'll call SecondaryCompare() to break the deadlock. +// SecondaryCompare() uses the same comparison functions again, but using the +// secondary key and potentially with different criteria (eg secondary sort +// order might be different to primary). The viewSortInfo::isSecondarySort +// flag lets the comparison function know not to call SecondaryCompare() +// again (and again and again)... +class viewSortInfo { + public: + nsMsgDBView* view; + nsIMsgDatabase* db; // Which db to use for collation compares. + bool isSecondarySort; + bool ascendingSort; +}; + +NS_IMPL_ISUPPORTS(nsMsgDBViewService, nsIMsgDBViewService) +NS_IMETHODIMP nsMsgDBViewService::InitializeDBViewStrings() { + nsMsgDBView::InitializeLiterals(); + nsMsgDBView::m_connectorPattern.Truncate(); + nsMsgDBView::mMessengerStringBundle = nullptr; + // Initialize date display format. + if (!nsMsgDBView::m_dateFormatsInitialized) { + nsMsgDBView::InitDisplayFormats(); + } + return NS_OK; +} + +NS_IMPL_ADDREF(nsMsgDBView) +NS_IMPL_RELEASE(nsMsgDBView) + +NS_INTERFACE_MAP_BEGIN(nsMsgDBView) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMsgDBView) + NS_INTERFACE_MAP_ENTRY(nsIMsgDBView) + NS_INTERFACE_MAP_ENTRY(nsIDBChangeListener) + NS_INTERFACE_MAP_ENTRY(nsITreeView) + NS_INTERFACE_MAP_ENTRY(nsIJunkMailClassificationListener) +NS_INTERFACE_MAP_END + +nsMsgDBView::nsMsgDBView() { + // Member initializers and constructor code. + m_sortValid = false; + m_checkedCustomColumns = false; + m_sortOrder = nsMsgViewSortOrder::none; + m_sortType = nsMsgViewSortType::byNone; + m_viewFlags = nsMsgViewFlagsType::kNone; + m_secondarySort = nsMsgViewSortType::byId; + m_secondarySortOrder = nsMsgViewSortOrder::ascending; + m_cachedMsgKey = nsMsgKey_None; + m_currentlyDisplayedMsgKey = nsMsgKey_None; + m_currentlyDisplayedViewIndex = nsMsgViewIndex_None; + mNumSelectedRows = 0; + mSuppressMsgDisplay = false; + mSuppressCommandUpdating = false; + mSuppressChangeNotification = false; + mSummarizeFailed = false; + mSelectionSummarized = false; + + mIsNews = false; + mIsRss = false; + mIsXFVirtual = false; + mDeleteModel = nsMsgImapDeleteModels::MoveToTrash; + m_deletingRows = false; + mNumMessagesRemainingInBatch = 0; + mShowSizeInLines = false; + mSortThreadsByRoot = false; + + // mCommandsNeedDisablingBecauseOfSelection - A boolean that tell us if we + // needed to disable commands because of what's selected. If we're offline + // w/o a downloaded msg selected, or a dummy message was selected. + mCommandsNeedDisablingBecauseOfSelection = false; + mRemovingRow = false; + m_saveRestoreSelectionDepth = 0; + mRecentlyDeletedArrayIndex = 0; +} + +void nsMsgDBView::InitializeLiterals() { + // Priority strings. + GetString(u"priorityHighest", kHighestPriorityString); + GetString(u"priorityHigh", kHighPriorityString); + GetString(u"priorityLowest", kLowestPriorityString); + GetString(u"priorityLow", kLowPriorityString); + GetString(u"priorityNormal", kNormalPriorityString); + + GetString(u"read", kReadString); + GetString(u"replied", kRepliedString); + GetString(u"forwarded", kForwardedString); + GetString(u"redirected", kRedirectedString); + GetString(u"new", kNewString); + + GetString(u"today", kTodayString); + GetString(u"yesterday", kYesterdayString); + GetString(u"last7Days", kLastWeekString); + GetString(u"last14Days", kTwoWeeksAgoString); + GetString(u"older", kOldMailString); + GetString(u"futureDate", kFutureDateString); +} + +nsMsgDBView::~nsMsgDBView() { + if (m_db) m_db->RemoveListener(this); +} + +// Helper function used to fetch strings from the messenger string bundle +void nsMsgDBView::GetString(const char16_t* aStringName, nsAString& aValue) { + nsresult res = NS_ERROR_UNEXPECTED; + + if (!nsMsgDBView::mMessengerStringBundle) { + static const char propertyURL[] = MESSENGER_STRING_URL; + nsCOMPtr<nsIStringBundleService> sBundleService = + mozilla::components::StringBundle::Service(); + + if (sBundleService) + res = sBundleService->CreateBundle( + propertyURL, getter_AddRefs(nsMsgDBView::mMessengerStringBundle)); + } + + if (nsMsgDBView::mMessengerStringBundle) + res = mMessengerStringBundle->GetStringFromName( + NS_ConvertUTF16toUTF8(aStringName).get(), aValue); + + if (NS_FAILED(res)) { + aValue = aStringName; + } +} + +// Helper function used to fetch localized strings from the prefs +nsresult nsMsgDBView::GetPrefLocalizedString(const char* aPrefName, + nsString& aResult) { + nsresult rv = NS_OK; + nsCOMPtr<nsIPrefBranch> prefBranch; + nsCOMPtr<nsIPrefLocalizedString> pls; + nsString ucsval; + + prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = prefBranch->GetComplexValue( + aPrefName, NS_GET_IID(nsIPrefLocalizedString), getter_AddRefs(pls)); + NS_ENSURE_SUCCESS(rv, rv); + pls->ToString(getter_Copies(ucsval)); + aResult = ucsval.get(); + return rv; +} + +nsresult nsMsgDBView::AppendKeywordProperties(const nsACString& keywords, + nsAString& properties, + bool* tagAdded) { + *tagAdded = false; + // Get the top most keyword's CSS selector and append that as a property. + nsresult rv; + if (!mTagService) { + mTagService = do_GetService("@mozilla.org/messenger/tagservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCString topKey; + rv = mTagService->GetTopKey(keywords, topKey); + NS_ENSURE_SUCCESS(rv, rv); + if (topKey.IsEmpty()) return NS_OK; + + nsString selector; + rv = mTagService->GetSelectorForKey(topKey, selector); + if (NS_SUCCEEDED(rv)) { + *tagAdded = true; + properties.Append(' '); + properties.Append(selector); + } + return rv; +} + +/////////////////////////////////////////////////////////////////////////// +// nsITreeView Implementation Methods (and helper methods) +/////////////////////////////////////////////////////////////////////////// + +static nsresult GetDisplayNameInAddressBook(const nsACString& emailAddress, + nsAString& displayName) { + nsresult rv; + nsCOMPtr<nsIAbManager> abManager( + do_GetService("@mozilla.org/abmanager;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbCard> cardForAddress; + rv = abManager->CardForEmailAddress(emailAddress, + getter_AddRefs(cardForAddress)); + NS_ENSURE_SUCCESS(rv, rv); + + if (cardForAddress) { + bool preferDisplayName = true; + rv = cardForAddress->GetPropertyAsBool("PreferDisplayName", true, + &preferDisplayName); + + if (NS_FAILED(rv) || preferDisplayName) + rv = cardForAddress->GetDisplayName(displayName); + } + + return rv; +} + +/* + * The unparsedString has following format: + * "version|displayname" + */ +static void GetCachedName(const nsCString& unparsedString, + int32_t displayVersion, nsACString& cachedName) { + nsresult err; + + // Get version #. + int32_t cachedVersion = unparsedString.ToInteger(&err); + if (cachedVersion != displayVersion) return; + + // Get cached name. + int32_t pos = unparsedString.FindChar('|'); + if (pos != kNotFound) cachedName = Substring(unparsedString, pos + 1); +} + +static void UpdateCachedName(nsIMsgDBHdr* aHdr, const char* header_field, + const nsAString& newName) { + nsCString newCachedName; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + int32_t currentDisplayNameVersion = 0; + + prefs->GetIntPref("mail.displayname.version", ¤tDisplayNameVersion); + + // Save version number. + newCachedName.AppendInt(currentDisplayNameVersion); + newCachedName.Append('|'); + + // Save name. + newCachedName.Append(NS_ConvertUTF16toUTF8(newName)); + + aHdr->SetStringProperty(header_field, newCachedName); +} + +nsresult nsMsgDBView::FetchAuthor(nsIMsgDBHdr* aHdr, nsAString& aSenderString) { + nsCString unparsedAuthor; + bool showCondensedAddresses = false; + int32_t currentDisplayNameVersion = 0; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + + prefs->GetIntPref("mail.displayname.version", ¤tDisplayNameVersion); + prefs->GetBoolPref("mail.showCondensedAddresses", &showCondensedAddresses); + + aHdr->GetStringProperty("sender_name", unparsedAuthor); + + // If the author is already computed, use it. + if (!unparsedAuthor.IsEmpty()) { + nsCString cachedDisplayName; + GetCachedName(unparsedAuthor, currentDisplayNameVersion, cachedDisplayName); + if (!cachedDisplayName.IsEmpty()) { + CopyUTF8toUTF16(cachedDisplayName, aSenderString); + return NS_OK; + } + } + + nsCString author; + (void)aHdr->GetAuthor(getter_Copies(author)); + + nsCString headerCharset; + aHdr->GetEffectiveCharset(headerCharset); + + nsString name; + nsCString emailAddress; + nsCOMArray<msgIAddressObject> addresses = + EncodedHeader(author, headerCharset.get()); + bool multipleAuthors = addresses.Length() > 1; + + ExtractFirstAddress(addresses, name, emailAddress); + + if (showCondensedAddresses) + GetDisplayNameInAddressBook(emailAddress, aSenderString); + + if (aSenderString.IsEmpty()) { + // We can't use the display name in the card; use the name contained in + // the header or email address. + if (name.IsEmpty()) { + CopyUTF8toUTF16(emailAddress, aSenderString); + } else { + int32_t atPos; + if ((atPos = name.FindChar('@')) == kNotFound || + name.FindChar('.', atPos) == kNotFound) { + aSenderString = name; + } else { + // Found @ followed by a dot, so this looks like a spoofing case. + aSenderString = name; + aSenderString.AppendLiteral(" <"); + AppendUTF8toUTF16(emailAddress, aSenderString); + aSenderString.Append('>'); + } + } + } + + if (multipleAuthors) { + aSenderString.AppendLiteral(" "); + nsAutoString val; + GetString(u"andOthers", val); + aSenderString.Append(val); + } + + UpdateCachedName(aHdr, "sender_name", aSenderString); + + return NS_OK; +} + +nsresult nsMsgDBView::FetchAccount(nsIMsgDBHdr* aHdr, nsAString& aAccount) { + nsCString accountKey; + nsresult rv = aHdr->GetAccountKey(getter_Copies(accountKey)); + + // Cache the account manager? + nsCOMPtr<nsIMsgAccountManager> accountManager( + do_GetService("@mozilla.org/messenger/account-manager;1", &rv)); + + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgAccount> account; + nsCOMPtr<nsIMsgIncomingServer> server; + if (!accountKey.IsEmpty()) + rv = accountManager->GetAccount(accountKey, getter_AddRefs(account)); + + if (account) { + account->GetIncomingServer(getter_AddRefs(server)); + } else { + nsCOMPtr<nsIMsgFolder> folder; + aHdr->GetFolder(getter_AddRefs(folder)); + if (folder) folder->GetServer(getter_AddRefs(server)); + } + + if (server) + server->GetPrettyName(aAccount); + else + CopyASCIItoUTF16(accountKey, aAccount); + + return NS_OK; +} + +nsresult nsMsgDBView::FetchRecipients(nsIMsgDBHdr* aHdr, + nsAString& aRecipientsString) { + nsCString recipients; + int32_t currentDisplayNameVersion = 0; + bool showCondensedAddresses = false; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + + prefs->GetIntPref("mail.displayname.version", ¤tDisplayNameVersion); + prefs->GetBoolPref("mail.showCondensedAddresses", &showCondensedAddresses); + + aHdr->GetStringProperty("recipient_names", recipients); + + if (!recipients.IsEmpty()) { + nsCString cachedRecipients; + GetCachedName(recipients, currentDisplayNameVersion, cachedRecipients); + + // Recipients have already been cached, check if the addressbook + // was changed after cache. + if (!cachedRecipients.IsEmpty()) { + CopyUTF8toUTF16(cachedRecipients, aRecipientsString); + return NS_OK; + } + } + + nsCString unparsedRecipients; + nsresult rv = aHdr->GetRecipients(getter_Copies(unparsedRecipients)); + + nsCString headerCharset; + aHdr->GetEffectiveCharset(headerCharset); + + nsTArray<nsString> names; + nsTArray<nsCString> emails; + ExtractAllAddresses(EncodedHeader(unparsedRecipients, headerCharset.get()), + names, UTF16ArrayAdapter<>(emails)); + + uint32_t numAddresses = names.Length(); + + nsCOMPtr<nsIAbManager> abManager( + do_GetService("@mozilla.org/abmanager;1", &rv)); + + NS_ENSURE_SUCCESS(rv, NS_OK); + + // Go through each email address in the recipients and compute its + // display name. + for (uint32_t i = 0; i < numAddresses; i++) { + nsString recipient; + nsCString& curAddress = emails[i]; + nsString& curName = names[i]; + + if (showCondensedAddresses) + GetDisplayNameInAddressBook(curAddress, recipient); + + if (recipient.IsEmpty()) { + // We can't use the display name in the card; use the name contained in + // the header or email address. + if (curName.IsEmpty()) { + CopyUTF8toUTF16(curAddress, recipient); + } else { + int32_t atPos; + if ((atPos = curName.FindChar('@')) == kNotFound || + curName.FindChar('.', atPos) == kNotFound) { + recipient = curName; + } else { + // Found @ followed by a dot, so this looks like a spoofing case. + recipient = curName; + recipient.AppendLiteral(" <"); + AppendUTF8toUTF16(curAddress, recipient); + recipient.Append('>'); + } + } + } + + // Add ', ' between each recipient. + if (i != 0) aRecipientsString.AppendLiteral(u", "); + + aRecipientsString.Append(recipient); + } + + if (numAddresses == 0 && unparsedRecipients.FindChar(':') != kNotFound) { + // No addresses and a colon, so an empty group like + // "undisclosed-recipients: ;". + // Add group name so at least something displays. + nsString group; + CopyUTF8toUTF16(unparsedRecipients, group); + aRecipientsString.Assign(group); + } + + UpdateCachedName(aHdr, "recipient_names", aRecipientsString); + + return NS_OK; +} + +nsresult nsMsgDBView::FetchSubject(nsIMsgDBHdr* aMsgHdr, uint32_t aFlags, + nsAString& aValue) { + if (aFlags & nsMsgMessageFlags::HasRe) { + nsString subject; + aMsgHdr->GetMime2DecodedSubject(subject); + aValue.AssignLiteral("Re: "); + aValue.Append(subject); + } else { + aMsgHdr->GetMime2DecodedSubject(aValue); + } + + return NS_OK; +} + +// In case we want to play around with the date string, I've broken it out into +// a separate routine. Set rcvDate to true to get the Received: date instead +// of the Date: date. +nsresult nsMsgDBView::FetchDate(nsIMsgDBHdr* aHdr, nsAString& aDateString, + bool rcvDate) { + PRTime dateOfMsg; + PRTime dateOfMsgLocal; + uint32_t rcvDateSecs; + nsresult rv; + + // Silently return Date: instead if Received: is unavailable. + if (rcvDate) { + rv = aHdr->GetUint32Property("dateReceived", &rcvDateSecs); + if (rcvDateSecs != 0) Seconds2PRTime(rcvDateSecs, &dateOfMsg); + } + + if (!rcvDate || rcvDateSecs == 0) rv = aHdr->GetDate(&dateOfMsg); + NS_ENSURE_SUCCESS(rv, rv); + + PRTime currentTime = PR_Now(); + PRExplodedTime explodedCurrentTime; + PR_ExplodeTime(currentTime, PR_LocalTimeParameters, &explodedCurrentTime); + PRExplodedTime explodedMsgTime; + PR_ExplodeTime(dateOfMsg, PR_LocalTimeParameters, &explodedMsgTime); + + // If the message is from today, don't show the date, only the time (3:15 pm). + // If the message is from the last week, show the day of the week + // (Mon 3:15 pm). In all other cases, show the full date (03/19/01 3:15 pm). + + nsDateFormatSelectorComm dateFormat = m_dateFormatDefault; + if (explodedCurrentTime.tm_year == explodedMsgTime.tm_year && + explodedCurrentTime.tm_month == explodedMsgTime.tm_month && + explodedCurrentTime.tm_mday == explodedMsgTime.tm_mday) { + // Same day. + dateFormat = m_dateFormatToday; + } else if (currentTime > dateOfMsg) { + // The following chunk of code allows us to show a day instead of a number + // if the message was received within the last 7 days. i.e. Mon 5:10pm + // (depending on the mail.ui.display.dateformat.thisweek pref). + // The concrete format used is dependent on a preference setting + // (see InitDisplayFormats). + // Convert the times from GMT to local time + int64_t GMTLocalTimeShift = + PR_USEC_PER_SEC * int64_t(explodedCurrentTime.tm_params.tp_gmt_offset + + explodedCurrentTime.tm_params.tp_dst_offset); + currentTime += GMTLocalTimeShift; + dateOfMsgLocal = dateOfMsg + GMTLocalTimeShift; + + // Find the most recent midnight. + int64_t todaysMicroSeconds = currentTime % PR_USEC_PER_DAY; + int64_t mostRecentMidnight = currentTime - todaysMicroSeconds; + + // Most recent midnight minus 6 days. + int64_t mostRecentWeek = mostRecentMidnight - (PR_USEC_PER_DAY * 6); + + // Was the message sent during the last week? + if (dateOfMsgLocal >= mostRecentWeek) dateFormat = m_dateFormatThisWeek; + } + + mozilla::intl::DateTimeFormat::StyleBag style; + style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short); + switch (dateFormat) { + case kDateFormatNone: + rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg, + aDateString); + NS_ENSURE_SUCCESS(rv, rv); + break; + case kDateFormatLong: + style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long); + rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg, + aDateString); + NS_ENSURE_SUCCESS(rv, rv); + break; + case kDateFormatShort: + style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short); + rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg, + aDateString); + NS_ENSURE_SUCCESS(rv, rv); + break; + case kDateFormatWeekday: { + // We want weekday + time. + nsAutoString timeString; + nsAutoString weekdayString; + rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg, + timeString); + NS_ENSURE_SUCCESS(rv, rv); + + mozilla::intl::DateTimeFormat::ComponentsBag components{}; + components.weekday = + mozilla::Some(mozilla::intl::DateTimeFormat::Text::Short); + rv = mozilla::intl::AppDateTimeFormat::Format( + components, &explodedMsgTime, weekdayString); + NS_ENSURE_SUCCESS(rv, rv); + + if (nsMsgDBView::m_connectorPattern.IsEmpty()) { + nsAutoCString locale; + AutoTArray<nsCString, 10> regionalPrefsLocales; + mozilla::intl::LocaleService::GetInstance()->GetRegionalPrefsLocales( + regionalPrefsLocales); + locale.Assign(regionalPrefsLocales[0]); + nsAutoCString str; + mozilla::intl::OSPreferences::GetInstance() + ->GetDateTimeConnectorPattern(locale, str); + nsMsgDBView::m_connectorPattern = NS_ConvertUTF8toUTF16(str); + } + + nsAutoString pattern(nsMsgDBView::m_connectorPattern); + int32_t ind = pattern.Find(u"{1}"_ns); + if (ind != kNotFound) { + pattern.Replace(ind, 3, weekdayString); + } + ind = pattern.Find(u"{0}"_ns); + if (ind != kNotFound) { + pattern.Replace(ind, 3, timeString); + } + aDateString = pattern; + break; + } + + default: + break; + } + + return rv; +} + +nsresult nsMsgDBView::FetchStatus(uint32_t aFlags, nsAString& aStatusString) { + if (aFlags & nsMsgMessageFlags::Replied) + aStatusString = kRepliedString; + else if (aFlags & nsMsgMessageFlags::Forwarded) + aStatusString = kForwardedString; + else if (aFlags & nsMsgMessageFlags::Redirected) + aStatusString = kRedirectedString; + else if (aFlags & nsMsgMessageFlags::New) + aStatusString = kNewString; + else if (aFlags & nsMsgMessageFlags::Read) + aStatusString = kReadString; + + return NS_OK; +} + +nsresult nsMsgDBView::FetchSize(nsIMsgDBHdr* aHdr, nsAString& aSizeString) { + nsresult rv; + nsAutoString formattedSizeString; + uint32_t msgSize = 0; + + // For news, show the line count, not the size if the user wants so. + if (mShowSizeInLines) { + aHdr->GetLineCount(&msgSize); + formattedSizeString.AppendInt(msgSize); + } else { + uint32_t flags = 0; + + aHdr->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Partial) + aHdr->GetUint32Property("onlineSize", &msgSize); + + if (msgSize == 0) aHdr->GetMessageSize(&msgSize); + + rv = FormatFileSize(msgSize, true, formattedSizeString); + NS_ENSURE_SUCCESS(rv, rv); + } + + aSizeString = formattedSizeString; + // The formattingString Length includes the null terminator byte! + if (!formattedSizeString.Last()) + aSizeString.SetLength(formattedSizeString.Length() - 1); + + return NS_OK; +} + +nsresult nsMsgDBView::FetchPriority(nsIMsgDBHdr* aHdr, + nsAString& aPriorityString) { + nsMsgPriorityValue priority = nsMsgPriority::notSet; + aHdr->GetPriority(&priority); + + switch (priority) { + case nsMsgPriority::highest: + aPriorityString = kHighestPriorityString; + break; + case nsMsgPriority::high: + aPriorityString = kHighPriorityString; + break; + case nsMsgPriority::low: + aPriorityString = kLowPriorityString; + break; + case nsMsgPriority::lowest: + aPriorityString = kLowestPriorityString; + break; + case nsMsgPriority::normal: + aPriorityString = kNormalPriorityString; + break; + default: + break; + } + + return NS_OK; +} + +nsresult nsMsgDBView::FetchKeywords(nsIMsgDBHdr* aHdr, + nsACString& keywordString) { + NS_ENSURE_ARG_POINTER(aHdr); + nsresult rv = NS_OK; + if (!mTagService) { + mTagService = do_GetService("@mozilla.org/messenger/tagservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + nsCString keywords; + aHdr->GetStringProperty("keywords", keywords); + keywordString = keywords; + return NS_OK; +} + +// If the row is a collapsed thread, we optionally roll-up the keywords in all +// the messages in the thread, otherwise, return just the keywords for the row. +nsresult nsMsgDBView::FetchRowKeywords(nsMsgViewIndex aRow, nsIMsgDBHdr* aHdr, + nsACString& keywordString) { + nsresult rv = FetchKeywords(aHdr, keywordString); + NS_ENSURE_SUCCESS(rv, rv); + + bool cascadeKeywordsUp = true; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + prefs->GetBoolPref("mailnews.display_reply_tag_colors_for_collapsed_threads", + &cascadeKeywordsUp); + + if ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) && + cascadeKeywordsUp) { + if ((m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD) && + (m_flags[aRow] & nsMsgMessageFlags::Elided)) { + nsCOMPtr<nsIMsgThread> thread; + rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv) && thread) { + uint32_t numChildren; + thread->GetNumChildren(&numChildren); + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCString moreKeywords; + for (uint32_t index = 0; index < numChildren; index++) { + thread->GetChildHdrAt(index, getter_AddRefs(msgHdr)); + rv = FetchKeywords(msgHdr, moreKeywords); + NS_ENSURE_SUCCESS(rv, rv); + + if (!keywordString.IsEmpty() && !moreKeywords.IsEmpty()) + keywordString.Append(' '); + + keywordString.Append(moreKeywords); + } + } + } + } + + return rv; +} + +nsresult nsMsgDBView::FetchTags(nsIMsgDBHdr* aHdr, nsAString& aTagString) { + NS_ENSURE_ARG_POINTER(aHdr); + nsresult rv = NS_OK; + if (!mTagService) { + mTagService = do_GetService("@mozilla.org/messenger/tagservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsString tags; + nsCString keywords; + aHdr->GetStringProperty("keywords", keywords); + + nsTArray<nsCString> keywordsArray; + ParseString(keywords, ' ', keywordsArray); + nsAutoString tag; + + for (uint32_t i = 0; i < keywordsArray.Length(); i++) { + rv = mTagService->GetTagForKey(keywordsArray[i], tag); + if (NS_SUCCEEDED(rv) && !tag.IsEmpty()) { + if (!tags.IsEmpty()) tags.Append((char16_t)' '); + + tags.Append(tag); + } + } + + aTagString = tags; + return NS_OK; +} + +/** + * Lowercase the email and remove a possible plus addressing part. + * E.g. John+test@example.com -> john@example.com. + */ +static void ToLowerCaseDropPlusAddessing(nsCString& aEmail) { + ToLowerCase(aEmail); + int32_t indPlus; + if ((indPlus = aEmail.FindChar('+')) == kNotFound) return; + int32_t indAt; + indAt = aEmail.FindChar('@', indPlus); + if (indAt == kNotFound) return; + aEmail.ReplaceLiteral(indPlus, indAt - indPlus, ""); +} + +bool nsMsgDBView::IsOutgoingMsg(nsIMsgDBHdr* aHdr) { + nsString author; + aHdr->GetMime2DecodedAuthor(author); + + nsCString emailAddress; + nsString name; + ExtractFirstAddress(DecodedHeader(author), name, emailAddress); + ToLowerCaseDropPlusAddessing(emailAddress); + return mEmails.Contains(emailAddress); +} + +// If you call SaveAndClearSelection make sure to call RestoreSelection(), +// otherwise m_saveRestoreSelectionDepth will be incorrect and will lead to +// selection msg problems. +nsresult nsMsgDBView::SaveAndClearSelection(nsMsgKey* aCurrentMsgKey, + nsTArray<nsMsgKey>& aMsgKeyArray) { + // Always return a value in the first parameter. + if (aCurrentMsgKey) *aCurrentMsgKey = nsMsgKey_None; + + // We don't do anything on nested Save / Restore calls. + m_saveRestoreSelectionDepth++; + if (m_saveRestoreSelectionDepth != 1) return NS_OK; + + if (!mTreeSelection) return NS_OK; + + // First, freeze selection. + mTreeSelection->SetSelectEventsSuppressed(true); + + // Second, save the current index. + if (aCurrentMsgKey) { + int32_t currentIndex; + if (NS_SUCCEEDED(mTreeSelection->GetCurrentIndex(¤tIndex)) && + currentIndex >= 0 && uint32_t(currentIndex) < GetSize()) + *aCurrentMsgKey = m_keys[currentIndex]; + else + *aCurrentMsgKey = nsMsgKey_None; + } + + // Third, get an array of view indices for the selection. + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + int32_t numIndices = selection.Length(); + aMsgKeyArray.SetLength(numIndices); + + // Now store the msg key for each selected item. + nsMsgKey msgKey; + for (int32_t index = 0; index < numIndices; index++) { + msgKey = m_keys[selection[index]]; + aMsgKeyArray[index] = msgKey; + } + + // Clear the selection, we'll manually restore it later. + if (mTreeSelection) mTreeSelection->ClearSelection(); + + return NS_OK; +} + +nsresult nsMsgDBView::RestoreSelection(nsMsgKey aCurrentMsgKey, + nsTArray<nsMsgKey>& aMsgKeyArray) { + // We don't do anything on nested Save / Restore calls. + m_saveRestoreSelectionDepth--; + if (m_saveRestoreSelectionDepth) return NS_OK; + + // Don't assert. + if (!mTreeSelection) return NS_OK; + + // Turn our message keys into corresponding view indices. + int32_t arraySize = aMsgKeyArray.Length(); + nsMsgViewIndex currentViewPosition = nsMsgViewIndex_None; + nsMsgViewIndex newViewPosition = nsMsgViewIndex_None; + + // If we are threaded, we need to do a little more work + // we need to find (and expand) all the threads that contain messages + // that we had selected before. + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + for (int32_t index = 0; index < arraySize; index++) + FindKey(aMsgKeyArray[index], true /* expand */); + } + + for (int32_t index = 0; index < arraySize; index++) { + newViewPosition = FindKey(aMsgKeyArray[index], false); + // Add the index back to the selection. + if (newViewPosition != nsMsgViewIndex_None) + mTreeSelection->ToggleSelect(newViewPosition); + } + + // Make sure the currentView was preserved. + if (aCurrentMsgKey != nsMsgKey_None) + currentViewPosition = FindKey(aCurrentMsgKey, true); + + if (mJSTree) mJSTree->SetCurrentIndex(currentViewPosition); + + // Make sure the current message is once again visible in the thread pane + // so we don't have to go search for it in the thread pane + if (currentViewPosition != nsMsgViewIndex_None) { + if (mJSTree) { + mJSTree->EnsureRowIsVisible(currentViewPosition); + } else if (mTree) { + mTree->EnsureRowIsVisible(currentViewPosition); + } + } + + // Unfreeze selection. + mTreeSelection->SetSelectEventsSuppressed(false); + return NS_OK; +} + +nsresult nsMsgDBView::GenerateURIForMsgKey(nsMsgKey aMsgKey, + nsIMsgFolder* folder, + nsACString& aURI) { + NS_ENSURE_ARG(folder); + return folder->GenerateMessageURI(aMsgKey, aURI); +} + +nsresult nsMsgDBView::GetMessageEnumerator(nsIMsgEnumerator** enumerator) { + return m_db->EnumerateMessages(enumerator); +} + +NS_IMETHODIMP +nsMsgDBView::IsEditable(int32_t row, nsTreeColumn* col, bool* _retval) { + NS_ENSURE_ARG_POINTER(col); + NS_ENSURE_ARG_POINTER(_retval); + // Attempt to retrieve a custom column handler. If it exists call it and + // return. + const nsAString& colID = col->GetId(); + nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID); + + if (colHandler) { + colHandler->IsEditable(row, col, _retval); + return NS_OK; + } + + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetCellValue(int32_t row, nsTreeColumn* col, + const nsAString& value) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetCellText(int32_t row, nsTreeColumn* col, + const nsAString& value) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetRowCount(int32_t* aRowCount) { + *aRowCount = GetSize(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSelection(nsITreeSelection** aSelection) { + NS_IF_ADDREF(*aSelection = mTreeSelection); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetSelection(nsITreeSelection* aSelection) { + mTreeSelection = aSelection; + return NS_OK; +} + +nsresult nsMsgDBView::UpdateDisplayMessage(nsMsgViewIndex viewPosition) { + nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater( + do_QueryReferent(mCommandUpdater)); + if (!commandUpdater) return NS_OK; + + if (!IsValidIndex(viewPosition)) return NS_MSG_INVALID_DBVIEW_INDEX; + + // Get the subject and the folder for the message and inform the front + // end that we changed the message we are currently displaying. + nsresult rv; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgHdrForViewIndex(viewPosition, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString subject; + if (viewPosition >= (nsMsgViewIndex)m_flags.Length()) + return NS_MSG_INVALID_DBVIEW_INDEX; + FetchSubject(msgHdr, m_flags[viewPosition], subject); + + nsCString keywords; + rv = msgHdr->GetStringProperty("keywords", keywords); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFolder> folder = m_viewFolder ? m_viewFolder : m_folder; + + commandUpdater->DisplayMessageChanged(folder, subject, keywords); + + if (folder) { + if (viewPosition >= (nsMsgViewIndex)m_keys.Length()) + return NS_MSG_INVALID_DBVIEW_INDEX; + rv = folder->SetLastMessageLoaded(m_keys[viewPosition]); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SelectionChangedXPCOM() { + // If the currentSelection changed then we have a message to display - + // not if we are in the middle of deleting rows. + if (m_deletingRows) return NS_OK; + + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + + bool commandsNeedDisablingBecauseOfSelection = false; + + if (!selection.IsEmpty()) { + if (WeAreOffline()) + commandsNeedDisablingBecauseOfSelection = !OfflineMsgSelected(selection); + + if (!NonDummyMsgSelected(selection)) + commandsNeedDisablingBecauseOfSelection = true; + } + + bool selectionSummarized = false; + mSummarizeFailed = false; + // Let the front-end adjust the message pane appropriately with either + // the message body, or a summary of the selection. + nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater( + do_QueryReferent(mCommandUpdater)); + if (commandUpdater) { + commandUpdater->SummarizeSelection(&selectionSummarized); + // Check if the selection was not summarized, but we expected it to be, + // and if so, remember it so GetHeadersFromSelection won't include + // the messages in collapsed threads. + if (!selectionSummarized && + (selection.Length() > 1 || + (selection.Length() == 1 && + m_flags[selection[0]] & nsMsgMessageFlags::Elided && + OperateOnMsgsInCollapsedThreads()))) { + mSummarizeFailed = true; + } + } + + bool summaryStateChanged = selectionSummarized != mSelectionSummarized; + mSelectionSummarized = selectionSummarized; + + if (!mTreeSelection || selection.Length() != 1 || selectionSummarized) { + // If we have zero or multiple items selected, we shouldn't be displaying + // any message. + m_currentlyDisplayedMsgKey = nsMsgKey_None; + m_currentlyDisplayedMsgUri.Truncate(); + m_currentlyDisplayedViewIndex = nsMsgViewIndex_None; + } + + // Determine if we need to push command update notifications out to the UI. + // We need to push a command update notification iff, one of the following + // conditions are met + // (1) the selection went from 0 to 1 + // (2) it went from 1 to 0 + // (3) it went from 1 to many + // (4) it went from many to 1 or 0 + // (5) a different msg was selected - perhaps it was offline or not, + // matters only when we are offline + // (6) we did a forward/back, or went from having no history to having + // history - not sure how to tell this. + // (7) whether the selection was summarized or not changed. + + // I think we're going to need to keep track of whether forward/back were + // enabled/should be enabled, and when this changes, force a command update. + + if (!summaryStateChanged && + (selection.Length() == mNumSelectedRows || + (selection.Length() > 1 && mNumSelectedRows > 1)) && + commandsNeedDisablingBecauseOfSelection == + mCommandsNeedDisablingBecauseOfSelection) { + // Don't update commands if we're suppressing them, or if we're removing + // rows, unless it was the last row. + } else if (!mSuppressCommandUpdating && commandUpdater && + (!mRemovingRow || GetSize() == 0)) { + commandUpdater->UpdateCommandStatus(); + } + + mCommandsNeedDisablingBecauseOfSelection = + commandsNeedDisablingBecauseOfSelection; + mNumSelectedRows = selection.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetRowProperties(int32_t index, nsAString& properties) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + // This is where we tell the tree to apply styles to a particular row. + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = NS_OK; + + rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr)); + + if (NS_FAILED(rv) || !msgHdr) { + ClearHdrCache(); + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + if (IsOutgoingMsg(msgHdr)) properties.AppendLiteral(" outgoing"); + + nsCString keywordProperty; + FetchRowKeywords(index, msgHdr, keywordProperty); + bool tagAdded = false; + if (!keywordProperty.IsEmpty()) { + AppendKeywordProperties(keywordProperty, properties, &tagAdded); + } + if (tagAdded) { + properties.AppendLiteral(" tagged"); + } else { + properties.AppendLiteral(" untagged"); + } + + uint32_t flags; + msgHdr->GetFlags(&flags); + + if (!(flags & nsMsgMessageFlags::Read)) + properties.AppendLiteral(" unread"); + else + properties.AppendLiteral(" read"); + + if (flags & nsMsgMessageFlags::Replied) properties.AppendLiteral(" replied"); + + if (flags & nsMsgMessageFlags::Forwarded) + properties.AppendLiteral(" forwarded"); + + if (flags & nsMsgMessageFlags::Redirected) + properties.AppendLiteral(" redirected"); + + if (flags & nsMsgMessageFlags::New) properties.AppendLiteral(" new"); + + if (m_flags[index] & nsMsgMessageFlags::Marked) + properties.AppendLiteral(" flagged"); + + // Give the custom column handlers a chance to style the row. + for (int i = 0; i < m_customColumnHandlers.Count(); i++) { + nsString extra; + m_customColumnHandlers[i]->GetRowProperties(index, extra); + if (!extra.IsEmpty()) { + properties.Append(' '); + properties.Append(extra); + } + } + + // For threaded display add the ignoreSubthread property to the + // subthread top row (this row). For non-threaded add it to all rows. + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) && + (flags & nsMsgMessageFlags::Ignored)) { + properties.AppendLiteral(" ignoreSubthread"); + } else { + bool ignored; + msgHdr->GetIsKilled(&ignored); + if (ignored) properties.AppendLiteral(" ignoreSubthread"); + } + + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder); + + if ((flags & nsMsgMessageFlags::Offline) || + (localFolder && !(flags & nsMsgMessageFlags::Partial))) + properties.AppendLiteral(" offline"); + + if (flags & nsMsgMessageFlags::Attachment) + properties.AppendLiteral(" attach"); + + if ((mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) && + (flags & nsMsgMessageFlags::IMAPDeleted)) + properties.AppendLiteral(" imapdeleted"); + + nsCString imageSize; + msgHdr->GetStringProperty("imageSize", imageSize); + if (!imageSize.IsEmpty()) properties.AppendLiteral(" hasimage"); + + nsCString junkScoreStr; + msgHdr->GetStringProperty("junkscore", junkScoreStr); + if (!junkScoreStr.IsEmpty()) { + if (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE) + properties.AppendLiteral(" junk"); + else + properties.AppendLiteral(" notjunk"); + + NS_ASSERTION(NS_SUCCEEDED(rv), "Converting junkScore to integer failed."); + } + + nsCOMPtr<nsIMsgThread> thread; + rv = GetThreadContainingIndex(index, getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv) && thread) { + uint32_t numUnreadChildren; + thread->GetNumUnreadChildren(&numUnreadChildren); + if (numUnreadChildren > 0) properties.AppendLiteral(" hasUnread"); + + // For threaded display add the ignore/watch properties to the + // thread top row. For non-threaded add it to all rows. + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) || + ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) && + (m_flags[index] & MSG_VIEW_FLAG_ISTHREAD))) { + thread->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Watched) + properties.AppendLiteral(" watch"); + if (flags & nsMsgMessageFlags::Ignored) + properties.AppendLiteral(" ignore"); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetColumnProperties(nsTreeColumn* col, nsAString& properties) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetCellProperties(int32_t aRow, nsTreeColumn* col, + nsAString& properties) { + if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX; + + // This is where we tell the tree to apply styles to a particular row + // i.e. if the row is an unread message... + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = NS_OK; + + rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr)); + + if (NS_FAILED(rv) || !msgHdr) { + ClearHdrCache(); + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + const nsAString& colID = col->GetId(); + nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID); + if (colHandler != nullptr) { + colHandler->GetCellProperties(aRow, col, properties); + } else if (colID[0] == 'c') { + // Correspondent. + if (IsOutgoingMsg(msgHdr)) + properties.AssignLiteral("outgoing"); + else + properties.AssignLiteral("incoming"); + } + + if (!properties.IsEmpty()) properties.Append(' '); + + properties.Append(mMessageType); + + uint32_t flags; + msgHdr->GetFlags(&flags); + + if (!(flags & nsMsgMessageFlags::Read)) + properties.AppendLiteral(" unread"); + else + properties.AppendLiteral(" read"); + + if (flags & nsMsgMessageFlags::Replied) properties.AppendLiteral(" replied"); + + if (flags & nsMsgMessageFlags::Forwarded) + properties.AppendLiteral(" forwarded"); + + if (flags & nsMsgMessageFlags::Redirected) + properties.AppendLiteral(" redirected"); + + if (flags & nsMsgMessageFlags::New) properties.AppendLiteral(" new"); + + if (m_flags[aRow] & nsMsgMessageFlags::Marked) + properties.AppendLiteral(" flagged"); + + // For threaded display add the ignoreSubthread property to the + // subthread top row (this row). For non-threaded add it to all rows. + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) && + (flags & nsMsgMessageFlags::Ignored)) { + properties.AppendLiteral(" ignoreSubthread"); + } else { + bool ignored; + msgHdr->GetIsKilled(&ignored); + if (ignored) properties.AppendLiteral(" ignoreSubthread"); + } + + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder); + + if ((flags & nsMsgMessageFlags::Offline) || + (localFolder && !(flags & nsMsgMessageFlags::Partial))) + properties.AppendLiteral(" offline"); + + if (flags & nsMsgMessageFlags::Attachment) + properties.AppendLiteral(" attach"); + + if ((mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) && + (flags & nsMsgMessageFlags::IMAPDeleted)) + properties.AppendLiteral(" imapdeleted"); + + nsCString imageSize; + msgHdr->GetStringProperty("imageSize", imageSize); + if (!imageSize.IsEmpty()) properties.AppendLiteral(" hasimage"); + + nsCString junkScoreStr; + msgHdr->GetStringProperty("junkscore", junkScoreStr); + if (!junkScoreStr.IsEmpty()) { + if (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE) + properties.AppendLiteral(" junk"); + else + properties.AppendLiteral(" notjunk"); + + NS_ASSERTION(NS_SUCCEEDED(rv), "Converting junkScore to integer failed."); + } + + nsCString keywords; + FetchRowKeywords(aRow, msgHdr, keywords); + bool tagAdded = false; + if (!keywords.IsEmpty()) { + AppendKeywordProperties(keywords, properties, &tagAdded); + } + if (tagAdded) { + properties.AppendLiteral(" tagged"); + } else { + properties.AppendLiteral(" untagged"); + } + + // This is a double fetch of the keywords property since we also fetch + // it for the tags - do we want to do this? + // I'm not sure anyone uses the kw- property, though it could be nice + // for people wanting to extend the thread pane. + nsCString keywordProperty; + msgHdr->GetStringProperty("keywords", keywordProperty); + if (!keywordProperty.IsEmpty()) { + NS_ConvertUTF8toUTF16 keywords(keywordProperty); + int32_t spaceIndex = 0; + do { + spaceIndex = keywords.FindChar(' '); + int32_t endOfKeyword = + (spaceIndex == -1) ? keywords.Length() : spaceIndex; + properties.AppendLiteral(" kw-"); + properties.Append(StringHead(keywords, endOfKeyword)); + if (spaceIndex > 0) keywords.Cut(0, endOfKeyword + 1); + } while (spaceIndex > 0); + } + +#ifdef SUPPORT_PRIORITY_COLORS + // Add special styles for priority. + nsMsgPriorityValue priority; + msgHdr->GetPriority(&priority); + switch (priority) { + case nsMsgPriority::highest: + properties.append(" priority-highest"); + break; + case nsMsgPriority::high: + properties.append(" priority-high"); + break; + case nsMsgPriority::low: + properties.append(" priority-low"); + break; + case nsMsgPriority::lowest: + properties.append(" priority-lowest"); + break; + default: + break; + } +#endif + + nsCOMPtr<nsIMsgThread> thread; + rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv) && thread) { + uint32_t numUnreadChildren; + thread->GetNumUnreadChildren(&numUnreadChildren); + if (numUnreadChildren > 0) properties.AppendLiteral(" hasUnread"); + + // For threaded display add the ignore/watch properties to the + // thread top row. For non-threaded add it to all rows. + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) || + ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) && + (m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD))) { + thread->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Watched) + properties.AppendLiteral(" watch"); + if (flags & nsMsgMessageFlags::Ignored) + properties.AppendLiteral(" ignore"); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::IsContainer(int32_t index, bool* _retval) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + uint32_t flags = m_flags[index]; + *_retval = !!(flags & MSG_VIEW_FLAG_HASCHILDREN); + } else { + *_retval = false; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::IsContainerOpen(int32_t index, bool* _retval) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + uint32_t flags = m_flags[index]; + *_retval = (flags & MSG_VIEW_FLAG_HASCHILDREN) && + !(flags & nsMsgMessageFlags::Elided); + } else { + *_retval = false; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::IsContainerEmpty(int32_t index, bool* _retval) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + uint32_t flags = m_flags[index]; + *_retval = !(flags & MSG_VIEW_FLAG_HASCHILDREN); + } else { + *_retval = false; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::IsSeparator(int32_t index, bool* _retval) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + *_retval = false; + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetParentIndex(int32_t rowIndex, int32_t* _retval) { + *_retval = -1; + + int32_t rowIndexLevel; + nsresult rv = GetLevel(rowIndex, &rowIndexLevel); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t i; + for (i = rowIndex; i >= 0; i--) { + int32_t l; + GetLevel(i, &l); + if (l < rowIndexLevel) { + *_retval = i; + break; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::HasNextSibling(int32_t rowIndex, int32_t afterIndex, + bool* _retval) { + *_retval = false; + + int32_t rowIndexLevel; + GetLevel(rowIndex, &rowIndexLevel); + + int32_t i; + int32_t count; + GetRowCount(&count); + for (i = afterIndex + 1; i < count; i++) { + int32_t l; + GetLevel(i, &l); + if (l < rowIndexLevel) break; + + if (l == rowIndexLevel) { + *_retval = true; + break; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetLevel(int32_t index, int32_t* _retval) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) + *_retval = m_levels[index]; + else + *_retval = 0; + + return NS_OK; +} + +// Search view will override this since headers can span db's. +nsresult nsMsgDBView::GetMsgHdrForViewIndex(nsMsgViewIndex index, + nsIMsgDBHdr** msgHdr) { + nsresult rv = NS_OK; + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + nsMsgKey key = m_keys[index]; + if (key == nsMsgKey_None || !m_db) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (key == m_cachedMsgKey) { + NS_IF_ADDREF(*msgHdr = m_cachedHdr); + } else { + rv = m_db->GetMsgHdrForKey(key, msgHdr); + if (NS_SUCCEEDED(rv)) { + m_cachedHdr = *msgHdr; + m_cachedMsgKey = key; + } + } + + return rv; +} + +void nsMsgDBView::InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr, + nsMsgKey msgKey, uint32_t flags, + uint32_t level) { + if ((int32_t)index < 0 || index > m_keys.Length()) { + // Something's gone wrong in a caller, but we have no clue why. + // Return without adding the header to the view. + NS_ERROR("Index for message header insertion out of array range!"); + return; + } + + m_keys.InsertElementAt(index, msgKey); + m_flags.InsertElementAt(index, flags); + m_levels.InsertElementAt(index, level); +} + +void nsMsgDBView::SetMsgHdrAt(nsIMsgDBHdr* hdr, nsMsgViewIndex index, + nsMsgKey msgKey, uint32_t flags, uint32_t level) { + m_keys[index] = msgKey; + m_flags[index] = flags; + m_levels[index] = level; +} + +nsresult nsMsgDBView::GetFolderForViewIndex(nsMsgViewIndex index, + nsIMsgFolder** aFolder) { + NS_IF_ADDREF(*aFolder = m_folder); + return NS_OK; +} + +nsresult nsMsgDBView::GetDBForViewIndex(nsMsgViewIndex index, + nsIMsgDatabase** db) { + NS_IF_ADDREF(*db = m_db); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetImageSrc(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) { + NS_ENSURE_ARG_POINTER(aCol); + // Attempt to retrieve a custom column handler. If it exists call it and + // return. + const nsAString& colID = aCol->GetId(); + nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID); + + if (colHandler) { + colHandler->GetImageSrc(aRow, aCol, aValue); + return NS_OK; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetCellValue(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) { + if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX; + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr)); + + if (NS_FAILED(rv) || !msgHdr) { + ClearHdrCache(); + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + const nsAString& colID = aCol->GetId(); + + aValue.Truncate(); + if (colID.IsEmpty()) return NS_OK; + + uint32_t flags; + msgHdr->GetFlags(&flags); + + // Provide a string "value" for cells that do not normally have text. + // Use empty string for the normal states "Read", "Not Starred", + // "No Attachment" and "Not Junk". + switch (colID.First()) { + case 'a': + if (colID.EqualsLiteral("attachmentCol") && + flags & nsMsgMessageFlags::Attachment) { + GetString(u"messageHasAttachment", aValue); + } + break; + case 'f': + if (colID.EqualsLiteral("flaggedCol") && + flags & nsMsgMessageFlags::Marked) { + GetString(u"messageHasFlag", aValue); + } + break; + case 'j': + if (colID.EqualsLiteral("junkStatusCol") && JunkControlsEnabled(aRow)) { + nsCString junkScoreStr; + msgHdr->GetStringProperty("junkscore", junkScoreStr); + // Only need to assign a real value for junk, it's empty already + // as it should be for non-junk. + if (!junkScoreStr.IsEmpty() && + (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE)) + aValue.AssignLiteral("messageJunk"); + + NS_ASSERTION(NS_SUCCEEDED(rv), + "Converting junkScore to integer failed."); + } + break; + case 't': + if (colID.EqualsLiteral("threadCol") && + (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { + // thread column + bool isContainer, isContainerEmpty, isContainerOpen; + IsContainer(aRow, &isContainer); + if (isContainer) { + IsContainerEmpty(aRow, &isContainerEmpty); + if (!isContainerEmpty) { + IsContainerOpen(aRow, &isContainerOpen); + GetString( + isContainerOpen ? u"messageExpanded" : u"messageCollapsed", + aValue); + } + } + } + break; + case 'u': + if (colID.EqualsLiteral("unreadButtonColHeader") && + !(flags & nsMsgMessageFlags::Read)) { + GetString(u"messageUnread", aValue); + } + break; + default: + aValue.Assign(colID); + break; + } + + return rv; +} + +void nsMsgDBView::RememberDeletedMsgHdr(nsIMsgDBHdr* msgHdr) { + nsCString messageId; + msgHdr->GetMessageId(getter_Copies(messageId)); + if (mRecentlyDeletedArrayIndex >= mRecentlyDeletedMsgIds.Length()) + mRecentlyDeletedMsgIds.AppendElement(messageId); + else + mRecentlyDeletedMsgIds[mRecentlyDeletedArrayIndex] = messageId; + + // Only remember last 20 deleted msgs. + mRecentlyDeletedArrayIndex = (mRecentlyDeletedArrayIndex + 1) % 20; +} + +bool nsMsgDBView::WasHdrRecentlyDeleted(nsIMsgDBHdr* msgHdr) { + nsCString messageId; + msgHdr->GetMessageId(getter_Copies(messageId)); + return mRecentlyDeletedMsgIds.Contains(messageId); +} + +/** + * CUSTOM COLUMNS. + */ + +// Add a custom column handler. +NS_IMETHODIMP +nsMsgDBView::AddColumnHandler(const nsAString& column, + nsIMsgCustomColumnHandler* handler) { + bool custColInSort = false; + size_t index = m_customColumnHandlerIDs.IndexOf(column); + + nsAutoString strColID(column); + + // Does not exist. + if (index == m_customColumnHandlerIDs.NoIndex) { + m_customColumnHandlerIDs.AppendElement(strColID); + m_customColumnHandlers.AppendObject(handler); + } else { + // Insert new handler into the appropriate place in the COMPtr array; + // no need to replace the column ID (it's the same). + m_customColumnHandlers.ReplaceObjectAt(handler, index); + } + + // Check if the column name matches any of the columns in + // m_sortColumns, and if so, set m_sortColumns[i].mColHandler + for (uint32_t i = 0; i < m_sortColumns.Length(); i++) { + MsgViewSortColumnInfo& sortInfo = m_sortColumns[i]; + if (sortInfo.mSortType == nsMsgViewSortType::byCustom && + sortInfo.mCustomColumnName.Equals(column)) { + custColInSort = true; + sortInfo.mColHandler = handler; + } + } + + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) + // Grouped view has its own ways. + return NS_OK; + + // This cust col is in sort columns, and all are now registered, so sort. + if (custColInSort && !CustomColumnsInSortAndNotRegistered()) + Sort(m_sortType, m_sortOrder); + + return NS_OK; +} + +// Remove a custom column handler. +NS_IMETHODIMP +nsMsgDBView::RemoveColumnHandler(const nsAString& aColID) { + // Here we should check if the column name matches any of the columns in + // m_sortColumns, and if so, clear m_sortColumns[i].mColHandler. + size_t index = m_customColumnHandlerIDs.IndexOf(aColID); + + if (index != m_customColumnHandlerIDs.NoIndex) { + m_customColumnHandlerIDs.RemoveElementAt(index); + m_customColumnHandlers.RemoveObjectAt(index); + // Check if the column name matches any of the columns in + // m_sortColumns, and if so, clear m_sortColumns[i].mColHandler. + for (uint32_t i = 0; i < m_sortColumns.Length(); i++) { + MsgViewSortColumnInfo& sortInfo = m_sortColumns[i]; + if (sortInfo.mSortType == nsMsgViewSortType::byCustom && + sortInfo.mCustomColumnName.Equals(aColID)) + sortInfo.mColHandler = nullptr; + } + + return NS_OK; + } + + // Can't remove a column that isn't currently custom handled. + return NS_ERROR_FAILURE; +} + +// TODO: NS_ENSURE_SUCCESS +nsIMsgCustomColumnHandler* nsMsgDBView::GetCurColumnHandler() { + return GetColumnHandler(m_curCustomColumn); +} + +NS_IMETHODIMP +nsMsgDBView::SetCurCustomColumn(const nsAString& aColID) { + m_curCustomColumn = aColID; + if (m_viewFolder) { + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(db)); + NS_ENSURE_SUCCESS(rv, rv); + folderInfo->SetProperty("customSortCol", aColID); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetCurCustomColumn(nsAString& result) { + result = m_curCustomColumn; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSecondaryCustomColumn(nsAString& result) { + result = m_secondaryCustomColumn; + return NS_OK; +} + +nsIMsgCustomColumnHandler* nsMsgDBView::GetColumnHandler( + const nsAString& colID) { + size_t index = m_customColumnHandlerIDs.IndexOf(colID); + return (index != m_customColumnHandlerIDs.NoIndex) + ? m_customColumnHandlers[index] + : nullptr; +} + +NS_IMETHODIMP +nsMsgDBView::GetColumnHandler(const nsAString& aColID, + nsIMsgCustomColumnHandler** aHandler) { + NS_ENSURE_ARG_POINTER(aHandler); + nsAutoString column(aColID); + NS_IF_ADDREF(*aHandler = GetColumnHandler(column)); + return (*aHandler) ? NS_OK : NS_ERROR_FAILURE; +} + +// Check if any active sort columns are custom. If none are custom, return false +// and go on as always. If any are custom, and all are not registered yet, +// return true (so that the caller can postpone sort). When the custom column +// observer is notified with MsgCreateDBView and registers the handler, +// AddColumnHandler will sort once all required handlers are set. +bool nsMsgDBView::CustomColumnsInSortAndNotRegistered() { + // The initial sort on view open has been started, subsequent user initiated + // sort callers can ignore verifying cust col registration. + m_checkedCustomColumns = true; + + // DecodeColumnSort must have already created m_sortColumns, otherwise we + // can't know, but go on anyway. + if (!m_sortColumns.Length()) return false; + + bool custColNotRegistered = false; + for (uint32_t i = 0; i < m_sortColumns.Length() && !custColNotRegistered; + i++) { + if (m_sortColumns[i].mSortType == nsMsgViewSortType::byCustom && + m_sortColumns[i].mColHandler == nullptr) + custColNotRegistered = true; + } + + return custColNotRegistered; +} +// END CUSTOM COLUMNS. + +NS_IMETHODIMP +nsMsgDBView::GetCellText(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) { + const nsAString& colID = aCol->GetId(); + + if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX; + + aValue.Truncate(); + + // Attempt to retrieve a custom column handler. If it exists call it and + // return. + nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID); + + if (colHandler) { + colHandler->GetCellText(aRow, aCol, aValue); + return NS_OK; + } + + return CellTextForColumn(aRow, colID, aValue); +} + +NS_IMETHODIMP +nsMsgDBView::CellTextForColumn(int32_t aRow, const nsAString& aColumnName, + nsAString& aValue) { + if (aColumnName.IsEmpty()) { + aValue.Truncate(); + return NS_OK; + } + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr)); + + if (NS_FAILED(rv) || !msgHdr) { + ClearHdrCache(); + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + nsCOMPtr<nsIMsgThread> thread; + + switch (aColumnName.First()) { + case 's': + if (aColumnName.EqualsLiteral("subjectCol")) + rv = FetchSubject(msgHdr, m_flags[aRow], aValue); + else if (aColumnName.EqualsLiteral("senderCol")) + rv = FetchAuthor(msgHdr, aValue); + else if (aColumnName.EqualsLiteral("sizeCol")) + rv = FetchSize(msgHdr, aValue); + else if (aColumnName.EqualsLiteral("statusCol")) { + uint32_t flags; + msgHdr->GetFlags(&flags); + rv = FetchStatus(flags, aValue); + } + break; + case 'r': + if (aColumnName.EqualsLiteral("recipientCol")) + rv = FetchRecipients(msgHdr, aValue); + else if (aColumnName.EqualsLiteral("receivedCol")) + rv = FetchDate(msgHdr, aValue, true); + break; + case 'd': + if (aColumnName.EqualsLiteral("dateCol")) rv = FetchDate(msgHdr, aValue); + break; + case 'c': + if (aColumnName.EqualsLiteral("correspondentCol")) { + if (IsOutgoingMsg(msgHdr)) + rv = FetchRecipients(msgHdr, aValue); + else + rv = FetchAuthor(msgHdr, aValue); + } + break; + case 'p': + if (aColumnName.EqualsLiteral("priorityCol")) + rv = FetchPriority(msgHdr, aValue); + break; + case 'a': + if (aColumnName.EqualsLiteral("accountCol")) + rv = FetchAccount(msgHdr, aValue); + break; + case 't': + // total msgs in thread column + if (aColumnName.EqualsLiteral("totalCol") && + m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + if (m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD) { + rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv) && thread) { + nsAutoString formattedCountString; + uint32_t numChildren; + thread->GetNumChildren(&numChildren); + formattedCountString.AppendInt(numChildren); + aValue.Assign(formattedCountString); + } + } + } else if (aColumnName.EqualsLiteral("tagsCol")) { + rv = FetchTags(msgHdr, aValue); + } + break; + case 'u': + // unread msgs in thread col + if (aColumnName.EqualsLiteral("unreadCol") && + m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + if (m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD) { + rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv) && thread) { + nsAutoString formattedCountString; + uint32_t numUnreadChildren; + thread->GetNumUnreadChildren(&numUnreadChildren); + if (numUnreadChildren > 0) { + formattedCountString.AppendInt(numUnreadChildren); + aValue.Assign(formattedCountString); + } + } + } + } + break; + case 'j': { + if (aColumnName.EqualsLiteral("junkStatusCol")) { + nsCString junkScoreStr; + msgHdr->GetStringProperty("junkscore", junkScoreStr); + CopyASCIItoUTF16(junkScoreStr, aValue); + } + break; + } + case 'i': { + if (aColumnName.EqualsLiteral("idCol")) { + nsAutoString keyString; + nsMsgKey key; + msgHdr->GetMessageKey(&key); + keyString.AppendInt((int64_t)key); + aValue.Assign(keyString); + } + break; + } + case 'l': { + if (aColumnName.EqualsLiteral("locationCol")) { + nsCOMPtr<nsIMsgFolder> folder; + nsresult rv = GetFolderForViewIndex(aRow, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + folder->GetPrettyName(aValue); + } + break; + } + default: + break; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::CellDataForColumns(int32_t aRow, + const nsTArray<nsString>& aColumnNames, + nsAString& aProperties, int32_t* aThreadLevel, + nsTArray<nsString>& _retval) { + nsresult rv; + _retval.Clear(); + + uint32_t count = aColumnNames.Length(); + _retval.SetCapacity(count); + for (nsString column : aColumnNames) { + nsString text; + rv = CellTextForColumn(aRow, column, text); + if (NS_FAILED(rv)) { + _retval.Clear(); + return rv; + } + _retval.AppendElement(text); + } + + rv = GetRowProperties(aRow, aProperties); + if (NS_FAILED(rv)) { + _retval.Clear(); + return rv; + } + + rv = GetLevel(aRow, aThreadLevel); + if (NS_FAILED(rv)) { + _retval.Clear(); + aProperties.Truncate(); + } + + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::SetTree(mozilla::dom::XULTreeElement* tree) { + mTree = tree; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetJSTree(nsIMsgJSTree* tree) { + mJSTree = tree; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::ToggleOpenState(int32_t index) { + uint32_t numChanged; + nsresult rv = ToggleExpansion(index, &numChanged); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::CycleHeader(nsTreeColumn* aCol) { + // Let HandleColumnClick() in threadPane.js handle it + // since it will set / clear the sort indicators. + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::CycleCell(int32_t row, nsTreeColumn* col) { + if (!IsValidIndex(row)) { + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + const nsAString& colID = col->GetId(); + + // Attempt to retrieve a custom column handler. If it exists call it and + // return. + nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID); + + if (colHandler) { + colHandler->CycleCell(row, col); + return NS_OK; + } + + // The cyclers below don't work for the grouped header dummy row, currently. + // A future implementation should consider both collapsed and expanded state. + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort && + m_flags[row] & MSG_VIEW_FLAG_DUMMY) + return NS_OK; + + if (colID.IsEmpty()) return NS_OK; + + switch (colID.First()) { + case 'u': + if (colID.EqualsLiteral("unreadButtonColHeader")) { + ApplyCommandToIndices(nsMsgViewCommandType::toggleMessageRead, + {(nsMsgViewIndex)row}); + } + break; + case 't': + if (colID.EqualsLiteral("threadCol")) { + ExpandAndSelectThreadByIndex(row, false); + } else if (colID.EqualsLiteral("tagsCol")) { + // XXX Do we want to keep this behaviour but switch it to tags? + // We could enumerate over the tags and go to the next one - it looks + // to me like this wasn't working before tags landed, so maybe not + // worth bothering with. + } + break; + case 'f': + if (colID.EqualsLiteral("flaggedCol")) { + // toggle the flagged status of the element at row. + if (m_flags[row] & nsMsgMessageFlags::Marked) { + ApplyCommandToIndices(nsMsgViewCommandType::unflagMessages, + {(nsMsgViewIndex)row}); + } else { + ApplyCommandToIndices(nsMsgViewCommandType::flagMessages, + {(nsMsgViewIndex)row}); + } + } + break; + case 'j': { + if (!colID.EqualsLiteral("junkStatusCol") || !JunkControlsEnabled(row)) { + return NS_OK; + } + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(row, getter_AddRefs(msgHdr)); + if (NS_SUCCEEDED(rv) && msgHdr) { + nsCString junkScoreStr; + rv = msgHdr->GetStringProperty("junkscore", junkScoreStr); + if (junkScoreStr.IsEmpty() || + (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_HAM_SCORE)) { + ApplyCommandToIndices(nsMsgViewCommandType::junk, + {(nsMsgViewIndex)row}); + } else { + ApplyCommandToIndices(nsMsgViewCommandType::unjunk, + {(nsMsgViewIndex)row}); + } + NS_ASSERTION(NS_SUCCEEDED(rv), + "Converting junkScore to integer failed."); + } + break; + } + default: + break; + } + + return NS_OK; +} + +/////////////////////////////////////////////////////////////////////////// +// end nsITreeView Implementation Methods +/////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +nsMsgDBView::Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder, + nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) { + m_viewFlags = viewFlags; + m_sortOrder = sortOrder; + m_sortType = sortType; + + nsresult rv; + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + + NS_ENSURE_SUCCESS(rv, rv); + bool userNeedsToAuthenticate = false; + // If we're PasswordProtectLocalCache, then we need to find out if the + // server is authenticated. + (void)accountManager->GetUserNeedsToAuthenticate(&userNeedsToAuthenticate); + if (userNeedsToAuthenticate) return NS_MSG_USER_NOT_AUTHENTICATED; + + if (folder) { + // Search view will have a null folder. + nsCOMPtr<nsIDBFolderInfo> folderInfo; + rv = folder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(m_db)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + msgDBService->RegisterPendingListener(folder, this); + m_folder = folder; + + if (!m_viewFolder) { + // There is never a viewFolder already set except for the single folder + // saved search case, where the backing folder m_folder is different from + // the m_viewFolder with its own dbFolderInfo state. + m_viewFolder = folder; + } + + SetMRUTimeForFolder(m_viewFolder); + + RestoreSortInfo(); + + // Determine if we are in a news folder or not. If yes, we'll show lines + // instead of size, and special icons in the thread pane. + nsCOMPtr<nsIMsgIncomingServer> server; + rv = folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + nsCString type; + rv = server->GetType(type); + NS_ENSURE_SUCCESS(rv, rv); + + // I'm not sure this is correct, because XF virtual folders with mixed news + // and mail can have this set. + mIsNews = type.LowerCaseEqualsLiteral("nntp"); + + // Default to a virtual folder if folder not set, since synthetic search + // views may not have a folder. + uint32_t folderFlags = nsMsgFolderFlags::Virtual; + if (folder) folder->GetFlags(&folderFlags); + + mIsXFVirtual = folderFlags & nsMsgFolderFlags::Virtual; + if (!mIsXFVirtual && type.LowerCaseEqualsLiteral("rss")) mIsRss = true; + + // Special case nntp --> news since we'll break themes if we try to be + // consistent. + if (mIsNews) + mMessageType.AssignLiteral("news"); + else + CopyUTF8toUTF16(type, mMessageType); + + GetImapDeleteModel(nullptr); + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefs) { + prefs->GetBoolPref("mailnews.sort_threads_by_root", &mSortThreadsByRoot); + if (mIsNews) + prefs->GetBoolPref("news.show_size_in_lines", &mShowSizeInLines); + } + } + + nsTArray<RefPtr<nsIMsgIdentity>> identities; + rv = accountManager->GetAllIdentities(identities); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto identity : identities) { + if (!identity) continue; + + nsCString email; + identity->GetEmail(email); + if (!email.IsEmpty()) { + ToLowerCaseDropPlusAddessing(email); + mEmails.PutEntry(email); + } + + identity->GetReplyTo(email); + if (!email.IsEmpty()) { + ToLowerCaseDropPlusAddessing(email); + mEmails.PutEntry(email); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::Close() { + int32_t oldSize = GetSize(); + // This is important, because the tree will ask us for our row count, which + // gets determined from the number of keys. + m_keys.Clear(); + // Be consistent. + m_flags.Clear(); + m_levels.Clear(); + + // Clear these out since they no longer apply if we're switching a folder + mJunkHdrs.Clear(); + + // This needs to happen after we remove all the keys, since RowCountChanged() + // will call our GetRowCount(). + if (mTree) mTree->RowCountChanged(0, -oldSize); + if (mJSTree) mJSTree->RowCountChanged(0, -oldSize); + + ClearHdrCache(); + if (m_db) { + m_db->RemoveListener(this); + m_db = nullptr; + } + if (m_folder) { + nsresult rv; + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + msgDBService->UnregisterPendingListener(this); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OpenWithHdrs(nsIMsgEnumerator* aHeaders, + nsMsgViewSortTypeValue aSortType, + nsMsgViewSortOrderValue aSortOrder, + nsMsgViewFlagsTypeValue aViewFlags, int32_t* aCount) { + NS_ASSERTION(false, "not implemented"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDBView::Init(nsIMessenger* aMessengerInstance, nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater) { + mMessengerWeak = do_GetWeakReference(aMessengerInstance); + mMsgWindowWeak = do_GetWeakReference(aMsgWindow); + mCommandUpdater = do_GetWeakReference(aCmdUpdater); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetSuppressCommandUpdating(bool aSuppressCommandUpdating) { + mSuppressCommandUpdating = aSuppressCommandUpdating; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSuppressCommandUpdating(bool* aSuppressCommandUpdating) { + *aSuppressCommandUpdating = mSuppressCommandUpdating; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetSuppressMsgDisplay(bool aSuppressDisplay) { + mSuppressMsgDisplay = aSuppressDisplay; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSuppressMsgDisplay(bool* aSuppressDisplay) { + *aSuppressDisplay = mSuppressMsgDisplay; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetUsingLines(bool* aUsingLines) { + *aUsingLines = mShowSizeInLines; + return NS_OK; +} + +int CompareViewIndices(const void* v1, const void* v2, void*) { + nsMsgViewIndex i1 = *(nsMsgViewIndex*)v1; + nsMsgViewIndex i2 = *(nsMsgViewIndex*)v2; + return i1 - i2; +} + +// Array<nsMsgViewIndex> getIndicesForSelection(); +NS_IMETHODIMP +nsMsgDBView::GetIndicesForSelection(nsTArray<nsMsgViewIndex>& indices) { + indices.Clear(); + if (mTreeSelection) { + int32_t viewSize = GetSize(); + int32_t count; + mTreeSelection->GetCount(&count); + indices.SetCapacity(count); + int32_t selectionCount; + mTreeSelection->GetRangeCount(&selectionCount); + for (int32_t i = 0; i < selectionCount; i++) { + int32_t startRange = -1; + int32_t endRange = -1; + mTreeSelection->GetRangeAt(i, &startRange, &endRange); + if (startRange >= 0 && startRange < viewSize) { + for (int32_t rangeIndex = startRange; + rangeIndex <= endRange && rangeIndex < viewSize; rangeIndex++) { + indices.AppendElement(rangeIndex); + } + } + } + + NS_ASSERTION(indices.Length() == uint32_t(count), + "selection count is wrong"); + } else { + // If there is no tree selection object then we must be in stand alone + // message mode. In that case the selected indices are really just the + // current message key. + nsMsgViewIndex viewIndex = FindViewIndex(m_currentlyDisplayedMsgKey); + if (viewIndex != nsMsgViewIndex_None) indices.AppendElement(viewIndex); + } + + return NS_OK; +} + +// Array<nsIMsgDBHdr> getSelectedMsgHdrs(); +NS_IMETHODIMP +nsMsgDBView::GetSelectedMsgHdrs(nsTArray<RefPtr<nsIMsgDBHdr>>& aResult) { + nsMsgViewIndexArray selection; + aResult.Clear(); + nsresult rv = GetIndicesForSelection(selection); + NS_ENSURE_SUCCESS(rv, rv); + return GetHeadersFromSelection(selection, aResult); +} + +NS_IMETHODIMP +nsMsgDBView::GetURIsForSelection(nsTArray<nsCString>& uris) { + uris.Clear(); + AutoTArray<RefPtr<nsIMsgDBHdr>, 1> messages; + nsresult rv = GetSelectedMsgHdrs(messages); + NS_ENSURE_SUCCESS(rv, rv); + uris.SetCapacity(messages.Length()); + for (nsIMsgDBHdr* msgHdr : messages) { + nsCString tmpUri; + nsCOMPtr<nsIMsgFolder> folder; + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFolder(getter_AddRefs(folder)); + rv = GenerateURIForMsgKey(msgKey, folder, tmpUri); + NS_ENSURE_SUCCESS(rv, rv); + uris.AppendElement(tmpUri); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetURIForViewIndex(nsMsgViewIndex index, nsACString& result) { + nsresult rv; + nsCOMPtr<nsIMsgFolder> folder = m_folder; + if (!folder) { + rv = GetFolderForViewIndex(index, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (index == nsMsgViewIndex_None || index >= m_flags.Length() || + m_flags[index] & MSG_VIEW_FLAG_DUMMY) { + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + return GenerateURIForMsgKey(m_keys[index], folder, result); +} + +NS_IMETHODIMP +nsMsgDBView::DoCommandWithFolder(nsMsgViewCommandTypeValue command, + nsIMsgFolder* destFolder) { + NS_ENSURE_ARG_POINTER(destFolder); + + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + + nsresult rv = NS_OK; + switch (command) { + case nsMsgViewCommandType::copyMessages: + case nsMsgViewCommandType::moveMessages: + rv = ApplyCommandToIndicesWithFolder(command, selection, destFolder); + NoteChange(0, 0, nsMsgViewNotificationCode::none); + break; + default: + NS_ASSERTION(false, "invalid command type"); + rv = NS_ERROR_UNEXPECTED; + break; + } + + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::DoCommand(nsMsgViewCommandTypeValue command) { + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + + nsresult rv = NS_OK; + switch (command) { + case nsMsgViewCommandType::downloadSelectedForOffline: + return DownloadForOffline(msgWindow, selection); + case nsMsgViewCommandType::downloadFlaggedForOffline: + return DownloadFlaggedForOffline(msgWindow); + case nsMsgViewCommandType::markMessagesRead: + case nsMsgViewCommandType::markMessagesUnread: + case nsMsgViewCommandType::toggleMessageRead: + case nsMsgViewCommandType::flagMessages: + case nsMsgViewCommandType::unflagMessages: + case nsMsgViewCommandType::deleteMsg: + case nsMsgViewCommandType::undeleteMsg: + case nsMsgViewCommandType::deleteNoTrash: + case nsMsgViewCommandType::markThreadRead: + case nsMsgViewCommandType::junk: + case nsMsgViewCommandType::unjunk: + rv = ApplyCommandToIndices(command, selection); + NoteChange(0, 0, nsMsgViewNotificationCode::none); + break; + case nsMsgViewCommandType::selectAll: + if (mTreeSelection) { + // If in threaded mode, we need to expand all before selecting. + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) + rv = ExpandAll(); + + mTreeSelection->SelectAll(); + if (mTree) mTree->Invalidate(); + if (mJSTree) mJSTree->Invalidate(); + } + break; + case nsMsgViewCommandType::selectThread: + rv = ExpandAndSelectThread(); + break; + case nsMsgViewCommandType::selectFlagged: + if (!mTreeSelection) { + rv = NS_ERROR_UNEXPECTED; + } else { + mTreeSelection->SetSelectEventsSuppressed(true); + mTreeSelection->ClearSelection(); + // XXX ExpandAll? + uint32_t numIndices = GetSize(); + for (uint32_t curIndex = 0; curIndex < numIndices; curIndex++) { + if (m_flags[curIndex] & nsMsgMessageFlags::Marked) + mTreeSelection->ToggleSelect(curIndex); + } + + mTreeSelection->SetSelectEventsSuppressed(false); + } + break; + case nsMsgViewCommandType::markAllRead: + if (m_folder) { + SetSuppressChangeNotifications(true); + rv = m_folder->MarkAllMessagesRead(msgWindow); + SetSuppressChangeNotifications(false); + if (mTree) mTree->Invalidate(); + if (mJSTree) mJSTree->Invalidate(); + } + break; + case nsMsgViewCommandType::toggleThreadWatched: + rv = ToggleWatched(selection); + break; + case nsMsgViewCommandType::expandAll: + rv = ExpandAll(); + m_viewFlags |= nsMsgViewFlagsType::kExpandAll; + SetViewFlags(m_viewFlags); + if (mTree) mTree->Invalidate(); + if (mJSTree) mJSTree->Invalidate(); + + break; + case nsMsgViewCommandType::collapseAll: + rv = CollapseAll(); + m_viewFlags &= ~nsMsgViewFlagsType::kExpandAll; + SetViewFlags(m_viewFlags); + if (mTree) mTree->Invalidate(); + if (mJSTree) mJSTree->Invalidate(); + + break; + default: + NS_ASSERTION(false, "invalid command type"); + rv = NS_ERROR_UNEXPECTED; + break; + } + + return rv; +} + +bool nsMsgDBView::ServerSupportsFilterAfterTheFact() { + // Cross folder virtual folders might not have a folder set. + if (!m_folder) return false; + + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = m_folder->GetServer(getter_AddRefs(server)); + // Unexpected. + if (NS_FAILED(rv)) return false; + + // Filter after the fact is implement using search so if you can't search, + // you can't filter after the fact. + bool canSearch; + rv = server->GetCanSearchMessages(&canSearch); + // Unexpected. + if (NS_FAILED(rv)) return false; + + return canSearch; +} + +NS_IMETHODIMP +nsMsgDBView::GetCommandStatus(nsMsgViewCommandTypeValue command, + bool* selectable_p, + nsMsgViewCommandCheckStateValue* selected_p) { + nsresult rv = NS_OK; + + bool haveSelection; + int32_t rangeCount; + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + // If range count is non-zero, we have at least one item selected, so we + // have a selection. + if (mTreeSelection && + NS_SUCCEEDED(mTreeSelection->GetRangeCount(&rangeCount)) && + rangeCount > 0) { + haveSelection = NonDummyMsgSelected(selection); + } else { + // If we don't have a tree selection we must be in stand alone mode. + haveSelection = IsValidIndex(m_currentlyDisplayedViewIndex); + } + + switch (command) { + case nsMsgViewCommandType::deleteMsg: + case nsMsgViewCommandType::deleteNoTrash: { + bool canDelete; + if (m_folder && + NS_SUCCEEDED(m_folder->GetCanDeleteMessages(&canDelete)) && + !canDelete) { + *selectable_p = false; + } else { + *selectable_p = haveSelection; + } + break; + } + case nsMsgViewCommandType::applyFilters: + // Disable if no messages. + // XXX todo, check that we have filters, and at least one is enabled. + *selectable_p = GetSize(); + if (*selectable_p) *selectable_p = ServerSupportsFilterAfterTheFact(); + + break; + case nsMsgViewCommandType::runJunkControls: + // Disable if no messages. + // XXX todo, check that we have JMC enabled? + *selectable_p = GetSize() && JunkControlsEnabled(nsMsgViewIndex_None); + break; + case nsMsgViewCommandType::deleteJunk: { + // Disable if no messages, or if we can't delete (like news and + // certain imap folders). + bool canDelete; + *selectable_p = + GetSize() && m_folder && + NS_SUCCEEDED(m_folder->GetCanDeleteMessages(&canDelete)) && canDelete; + break; + } + case nsMsgViewCommandType::markMessagesRead: + case nsMsgViewCommandType::markMessagesUnread: + case nsMsgViewCommandType::toggleMessageRead: + case nsMsgViewCommandType::flagMessages: + case nsMsgViewCommandType::unflagMessages: + case nsMsgViewCommandType::toggleThreadWatched: + case nsMsgViewCommandType::markThreadRead: + case nsMsgViewCommandType::downloadSelectedForOffline: + *selectable_p = haveSelection; + break; + case nsMsgViewCommandType::junk: + case nsMsgViewCommandType::unjunk: + *selectable_p = haveSelection && !selection.IsEmpty() && + JunkControlsEnabled(selection[0]); + break; + case nsMsgViewCommandType::cmdRequiringMsgBody: + *selectable_p = + haveSelection && (!WeAreOffline() || OfflineMsgSelected(selection)); + break; + case nsMsgViewCommandType::downloadFlaggedForOffline: + case nsMsgViewCommandType::markAllRead: + *selectable_p = true; + break; + default: + NS_ASSERTION(false, "invalid command type"); + rv = NS_ERROR_FAILURE; + } + + return rv; +} + +// This method needs to be overridden by the various view classes +// that have different kinds of threads. For example, in a +// threaded quick search db view, we'd only want to include children +// of the thread that fit the view (IMO). And when we have threaded +// cross folder views, we would include all the children of the +// cross-folder thread. +nsresult nsMsgDBView::ListCollapsedChildren( + nsMsgViewIndex viewIndex, nsTArray<RefPtr<nsIMsgDBHdr>>& messageArray) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCOMPtr<nsIMsgThread> thread; + GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr)); + if (!msgHdr) { + NS_ASSERTION(false, "couldn't find message to expand"); + return NS_MSG_MESSAGE_NOT_FOUND; + } + + nsresult rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread)); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t numChildren; + thread->GetNumChildren(&numChildren); + for (uint32_t i = 1; i < numChildren && NS_SUCCEEDED(rv); i++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = thread->GetChildHdrAt(i, getter_AddRefs(msgHdr)); + if (msgHdr) { + messageArray.AppendElement(msgHdr); + } + } + + return rv; +} + +bool nsMsgDBView::OperateOnMsgsInCollapsedThreads() { + if (!mJSTree && mTreeSelection) { + RefPtr<mozilla::dom::XULTreeElement> selTree; + mTreeSelection->GetTree(getter_AddRefs(selTree)); + // No tree means stand-alone message window. + if (!selTree) return false; + } + + nsresult rv = NS_OK; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, false); + + bool includeCollapsedMsgs = false; + prefBranch->GetBoolPref("mail.operate_on_msgs_in_collapsed_threads", + &includeCollapsedMsgs); + return includeCollapsedMsgs; +} + +nsresult nsMsgDBView::GetHeadersFromSelection( + nsTArray<nsMsgViewIndex> const& selection, + nsTArray<RefPtr<nsIMsgDBHdr>>& hdrs) { + hdrs.Clear(); + hdrs.SetCapacity(selection.Length()); // Best guess. + nsresult rv = NS_OK; + + // Don't include collapsed messages if the front end failed to summarize + // the selection. + bool includeCollapsedMsgs = + OperateOnMsgsInCollapsedThreads() && !mSummarizeFailed; + + for (nsMsgViewIndex viewIndex : selection) { + if (NS_FAILED(rv)) { + break; + } + if (viewIndex == nsMsgViewIndex_None) { + continue; + } + + uint32_t viewIndexFlags = m_flags[viewIndex]; + if (viewIndexFlags & MSG_VIEW_FLAG_DUMMY) { + // If collapsed dummy header selected, list its children. + if (includeCollapsedMsgs && viewIndexFlags & nsMsgMessageFlags::Elided && + m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) + rv = ListCollapsedChildren(viewIndex, hdrs); + + continue; + } + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr)); + if (NS_SUCCEEDED(rv) && msgHdr) { + hdrs.AppendElement(msgHdr); + if (includeCollapsedMsgs && viewIndexFlags & nsMsgMessageFlags::Elided && + viewIndexFlags & MSG_VIEW_FLAG_HASCHILDREN && + m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + rv = ListCollapsedChildren(viewIndex, hdrs); + } + } + } + + return rv; +} + +nsresult nsMsgDBView::CopyMessages(nsIMsgWindow* window, + nsTArray<nsMsgViewIndex> const& selection, + bool isMove, nsIMsgFolder* destFolder) { + if (m_deletingRows) { + NS_ASSERTION(false, "Last move did not complete"); + return NS_OK; + } + + nsresult rv; + NS_ENSURE_ARG_POINTER(destFolder); + + AutoTArray<RefPtr<nsIMsgDBHdr>, 1> hdrs; + rv = GetHeadersFromSelection(selection, hdrs); + NS_ENSURE_SUCCESS(rv, rv); + + m_deletingRows = isMove && mDeleteModel != nsMsgImapDeleteModels::IMAPDelete; + if (m_deletingRows) { + mIndicesToNoteChange.AppendElements(selection); + } + + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return copyService->CopyMessages(m_folder /* source folder */, hdrs, + destFolder, isMove, nullptr /* listener */, + window, true /* allow Undo */); +} + +nsresult nsMsgDBView::ApplyCommandToIndicesWithFolder( + nsMsgViewCommandTypeValue command, + nsTArray<nsMsgViewIndex> const& selection, nsIMsgFolder* destFolder) { + nsresult rv = NS_OK; + NS_ENSURE_ARG_POINTER(destFolder); + + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + switch (command) { + case nsMsgViewCommandType::copyMessages: + NS_ASSERTION(!(m_folder == destFolder), + "The source folder and the destination folder are the same"); + if (m_folder != destFolder) + rv = CopyMessages(msgWindow, selection, false /* isMove */, destFolder); + + break; + case nsMsgViewCommandType::moveMessages: + NS_ASSERTION(!(m_folder == destFolder), + "The source folder and the destination folder are the same"); + if (m_folder != destFolder) + rv = CopyMessages(msgWindow, selection, true /* isMove */, destFolder); + + break; + default: + NS_ASSERTION(false, "unhandled command"); + rv = NS_ERROR_UNEXPECTED; + break; + } + + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::ApplyCommandToIndices(nsMsgViewCommandTypeValue command, + nsTArray<nsMsgViewIndex> const& selection) { + if (selection.IsEmpty()) { + // Return quietly, just in case/ + return NS_OK; + } + + nsCOMPtr<nsIMsgFolder> folder; + nsresult rv = GetFolderForViewIndex(selection[0], getter_AddRefs(folder)); + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + if (command == nsMsgViewCommandType::deleteMsg) + return DeleteMessages(msgWindow, selection, false); + + if (command == nsMsgViewCommandType::deleteNoTrash) + return DeleteMessages(msgWindow, selection, true); + + nsTArray<nsMsgKey> imapUids; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder); + bool thisIsImapFolder = (imapFolder != nullptr); + nsCOMPtr<nsIJunkMailPlugin> junkPlugin; + + // If this is a junk command, get the junk plugin. + if (command == nsMsgViewCommandType::junk || + command == nsMsgViewCommandType::unjunk) { + // Get the folder from the first item; we assume that + // all messages in the view are from the same folder (no + // more junk status column in the 'search messages' dialog + // like in earlier versions...). + nsCOMPtr<nsIMsgIncomingServer> server; + rv = folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFilterPlugin> filterPlugin; + rv = server->GetSpamFilterPlugin(getter_AddRefs(filterPlugin)); + NS_ENSURE_SUCCESS(rv, rv); + + junkPlugin = do_QueryInterface(filterPlugin, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications, + false); + + // No sense going through the code that handles messages in collasped threads + // for mark thread read. + if (command == nsMsgViewCommandType::markThreadRead) { + for (nsMsgViewIndex viewIndex : selection) { + SetThreadOfMsgReadByIndex(viewIndex, imapUids, true); + } + } else { + // Turn the selection into an array of msg hdrs. This may include messages + // in collapsed threads + AutoTArray<RefPtr<nsIMsgDBHdr>, 1> messages; + rv = GetHeadersFromSelection(selection, messages); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t length = messages.Length(); + + if (thisIsImapFolder) { + imapUids.SetLength(length); + } + + for (uint32_t i = 0; i < length; i++) { + nsMsgKey msgKey; + nsCOMPtr<nsIMsgDBHdr> msgHdr(messages[i]); + msgHdr->GetMessageKey(&msgKey); + if (thisIsImapFolder) imapUids[i] = msgKey; + + switch (command) { + case nsMsgViewCommandType::junk: + mNumMessagesRemainingInBatch++; + mJunkHdrs.AppendElement(msgHdr); + rv = SetMsgHdrJunkStatus(junkPlugin.get(), msgHdr, + nsIJunkMailPlugin::JUNK); + break; + case nsMsgViewCommandType::unjunk: + mNumMessagesRemainingInBatch++; + mJunkHdrs.AppendElement(msgHdr); + rv = SetMsgHdrJunkStatus(junkPlugin.get(), msgHdr, + nsIJunkMailPlugin::GOOD); + break; + case nsMsgViewCommandType::toggleMessageRead: + case nsMsgViewCommandType::undeleteMsg: + case nsMsgViewCommandType::markMessagesRead: + case nsMsgViewCommandType::markMessagesUnread: + case nsMsgViewCommandType::unflagMessages: + case nsMsgViewCommandType::flagMessages: + // This is completely handled in the code below. + break; + default: + NS_ERROR("unhandled command"); + break; + } + } + + switch (command) { + case nsMsgViewCommandType::toggleMessageRead: { + if (messages.IsEmpty()) break; + + uint32_t msgFlags; + messages[0]->GetFlags(&msgFlags); + folder->MarkMessagesRead(messages, + !(msgFlags & nsMsgMessageFlags::Read)); + break; + } + case nsMsgViewCommandType::markMessagesRead: + case nsMsgViewCommandType::markMessagesUnread: + folder->MarkMessagesRead( + messages, command == nsMsgViewCommandType::markMessagesRead); + break; + case nsMsgViewCommandType::unflagMessages: + case nsMsgViewCommandType::flagMessages: + folder->MarkMessagesFlagged( + messages, command == nsMsgViewCommandType::flagMessages); + break; + default: + break; + } + + // Provide junk-related batch notifications. + if (command == nsMsgViewCommandType::junk || + command == nsMsgViewCommandType::unjunk) { + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) { + notifier->NotifyMsgsJunkStatusChanged(messages); + } + } + } + + folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications, true); + + if (thisIsImapFolder) { + imapMessageFlagsType flags = kNoImapMsgFlag; + bool addFlags = false; + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + switch (command) { + case nsMsgViewCommandType::markThreadRead: + flags |= kImapMsgSeenFlag; + addFlags = true; + break; + case nsMsgViewCommandType::undeleteMsg: + flags = kImapMsgDeletedFlag; + addFlags = false; + break; + case nsMsgViewCommandType::junk: + return imapFolder->StoreCustomKeywords(msgWindow, "Junk"_ns, + "NonJunk"_ns, imapUids, nullptr); + case nsMsgViewCommandType::unjunk: { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + GetHdrForFirstSelectedMessage(getter_AddRefs(msgHdr)); + uint32_t msgFlags = 0; + if (msgHdr) msgHdr->GetFlags(&msgFlags); + + if (msgFlags & nsMsgMessageFlags::IMAPDeleted) + imapFolder->StoreImapFlags(kImapMsgDeletedFlag, false, imapUids, + nullptr); + + return imapFolder->StoreCustomKeywords(msgWindow, "NonJunk"_ns, + "Junk"_ns, imapUids, nullptr); + } + default: + break; + } + + // Can't get here without thisIsImapThreadPane == TRUE. + if (flags != kNoImapMsgFlag) { + imapFolder->StoreImapFlags(flags, addFlags, imapUids, nullptr); + } + } + + return rv; +} + +/** + * View modifications methods by index. + */ + +// This method just removes the specified line from the view. It does +// NOT delete it from the database. +nsresult nsMsgDBView::RemoveByIndex(nsMsgViewIndex index) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + m_keys.RemoveElementAt(index); + m_flags.RemoveElementAt(index); + m_levels.RemoveElementAt(index); + + // The call to NoteChange() has to happen after we remove the key as + // NoteChange() will call RowCountChanged() which will call our GetRowCount(). + // An example where view is not the listener - D&D messages. + if (!m_deletingRows) + NoteChange(index, -1, nsMsgViewNotificationCode::insertOrDelete); + + return NS_OK; +} + +nsresult nsMsgDBView::DeleteMessages(nsIMsgWindow* window, + nsTArray<nsMsgViewIndex> const& selection, + bool deleteStorage) { + if (m_deletingRows) { + NS_WARNING("Last delete did not complete"); + return NS_OK; + } + + nsresult rv; + AutoTArray<RefPtr<nsIMsgDBHdr>, 1> hdrs; + rv = GetHeadersFromSelection(selection, hdrs); + NS_ENSURE_SUCCESS(rv, rv); + + const char* warnCollapsedPref = "mail.warn_on_collapsed_thread_operation"; + const char* warnShiftDelPref = "mail.warn_on_shift_delete"; + const char* warnNewsPref = "news.warn_on_delete"; + const char* warnTrashDelPref = "mail.warn_on_delete_from_trash"; + const char* activePref = nullptr; + nsString warningName; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + bool trashFolder = false; + rv = m_folder->GetFlag(nsMsgFolderFlags::Trash, &trashFolder); + NS_ENSURE_SUCCESS(rv, rv); + + if (trashFolder) { + bool pref = false; + prefBranch->GetBoolPref(warnTrashDelPref, &pref); + if (pref) { + activePref = warnTrashDelPref; + warningName.AssignLiteral("confirmMsgDelete.deleteFromTrash.desc"); + } + } + + if (!activePref && (selection.Length() != hdrs.Length())) { + bool pref = false; + prefBranch->GetBoolPref(warnCollapsedPref, &pref); + if (pref) { + activePref = warnCollapsedPref; + warningName.AssignLiteral("confirmMsgDelete.collapsed.desc"); + } + } + + if (!activePref && deleteStorage && !trashFolder) { + bool pref = false; + prefBranch->GetBoolPref(warnShiftDelPref, &pref); + if (pref) { + activePref = warnShiftDelPref; + warningName.AssignLiteral("confirmMsgDelete.deleteNoTrash.desc"); + } + } + + if (!activePref && mIsNews) { + bool pref = false; + prefBranch->GetBoolPref(warnNewsPref, &pref); + if (pref) { + activePref = warnNewsPref; + warningName.AssignLiteral("confirmMsgDelete.deleteNoTrash.desc"); + } + } + + if (activePref) { + nsCOMPtr<nsIPrompt> dialog; + + nsCOMPtr<nsIWindowWatcher> wwatch( + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = wwatch->GetNewPrompter(0, getter_AddRefs(dialog)); + NS_ENSURE_SUCCESS(rv, rv); + // "Don't ask..." - unchecked by default. + bool dontAsk = false; + int32_t buttonPressed = 0; + + nsString dialogTitle; + nsString confirmString; + nsString checkboxText; + nsString buttonApplyNowText; + GetString(u"confirmMsgDelete.title", dialogTitle); + GetString(u"confirmMsgDelete.dontAsk.label", checkboxText); + GetString(u"confirmMsgDelete.delete.label", buttonApplyNowText); + + GetString(warningName.get(), confirmString); + + const uint32_t buttonFlags = + (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_0) + + (nsIPrompt::BUTTON_TITLE_CANCEL * nsIPrompt::BUTTON_POS_1); + + rv = dialog->ConfirmEx(dialogTitle.get(), confirmString.get(), buttonFlags, + buttonApplyNowText.get(), nullptr, nullptr, + checkboxText.get(), &dontAsk, &buttonPressed); + NS_ENSURE_SUCCESS(rv, rv); + if (buttonPressed) return NS_ERROR_FAILURE; + + if (dontAsk) prefBranch->SetBoolPref(activePref, false); + } + + if (!deleteStorage) { + rv = m_folder->MarkMessagesRead(hdrs, true); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mDeleteModel != nsMsgImapDeleteModels::IMAPDelete) { + m_deletingRows = true; + } + + if (m_deletingRows) { + mIndicesToNoteChange.AppendElements(selection); + } + + rv = m_folder->DeleteMessages(hdrs, window, deleteStorage, false, nullptr, + true /* allow Undo */); + if (NS_FAILED(rv)) { + m_deletingRows = false; + } + + return rv; +} + +nsresult nsMsgDBView::DownloadForOffline( + nsIMsgWindow* window, nsTArray<nsMsgViewIndex> const& selection) { + nsresult rv = NS_OK; + nsTArray<RefPtr<nsIMsgDBHdr>> messages; + for (nsMsgViewIndex viewIndex : selection) { + nsMsgKey key = m_keys[viewIndex]; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = m_db->GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + if (msgHdr) { + uint32_t flags; + msgHdr->GetFlags(&flags); + if (!(flags & nsMsgMessageFlags::Offline)) { + messages.AppendElement(msgHdr); + } + } + } + + m_folder->DownloadMessagesForOffline(messages, window); + return rv; +} + +nsresult nsMsgDBView::DownloadFlaggedForOffline(nsIMsgWindow* window) { + nsresult rv = NS_OK; + nsTArray<RefPtr<nsIMsgDBHdr>> messages; + nsCOMPtr<nsIMsgEnumerator> enumerator; + rv = GetMessageEnumerator(getter_AddRefs(enumerator)); + if (NS_SUCCEEDED(rv) && enumerator) { + bool hasMore; + while (NS_SUCCEEDED(rv = enumerator->HasMoreElements(&hasMore)) && + hasMore) { + nsCOMPtr<nsIMsgDBHdr> header; + rv = enumerator->GetNext(getter_AddRefs(header)); + if (header && NS_SUCCEEDED(rv)) { + uint32_t flags; + header->GetFlags(&flags); + if ((flags & nsMsgMessageFlags::Marked) && + !(flags & nsMsgMessageFlags::Offline)) { + messages.AppendElement(header); + } + } + } + } + + m_folder->DownloadMessagesForOffline(messages, window); + return rv; +} + +// Read/unread handling. +nsresult nsMsgDBView::ToggleReadByIndex(nsMsgViewIndex index) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + return SetReadByIndex(index, !(m_flags[index] & nsMsgMessageFlags::Read)); +} + +nsresult nsMsgDBView::SetReadByIndex(nsMsgViewIndex index, bool read) { + nsresult rv; + + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (read) { + OrExtraFlag(index, nsMsgMessageFlags::Read); + // MarkRead() will clear this flag in the db and then call OnKeyChange(), + // but because we are the instigator of the change we'll ignore the change. + // So we need to clear it in m_flags to keep the db and m_flags in sync. + AndExtraFlag(index, ~nsMsgMessageFlags::New); + } else { + AndExtraFlag(index, ~nsMsgMessageFlags::Read); + } + + nsCOMPtr<nsIMsgDatabase> dbToUse; + rv = GetDBForViewIndex(index, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = dbToUse->MarkRead(m_keys[index], read, this); + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + nsMsgViewIndex threadIndex = GetThreadIndex(index); + if (threadIndex != index) + NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed); + } + + return rv; +} + +nsresult nsMsgDBView::SetThreadOfMsgReadByIndex( + nsMsgViewIndex index, nsTArray<nsMsgKey>& keysMarkedRead, bool /*read*/) { + nsresult rv; + + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + rv = MarkThreadOfMsgRead(m_keys[index], index, keysMarkedRead, true); + return rv; +} + +nsresult nsMsgDBView::SetFlaggedByIndex(nsMsgViewIndex index, bool mark) { + nsresult rv; + + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + nsCOMPtr<nsIMsgDatabase> dbToUse; + rv = GetDBForViewIndex(index, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + + if (mark) + OrExtraFlag(index, nsMsgMessageFlags::Marked); + else + AndExtraFlag(index, ~nsMsgMessageFlags::Marked); + + rv = dbToUse->MarkMarked(m_keys[index], mark, this); + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + return rv; +} + +nsresult nsMsgDBView::SetMsgHdrJunkStatus(nsIJunkMailPlugin* aJunkPlugin, + nsIMsgDBHdr* aMsgHdr, + nsMsgJunkStatus aNewClassification) { + // Get the old junk score. + nsCString junkScoreStr; + nsresult rv = aMsgHdr->GetStringProperty("junkscore", junkScoreStr); + + // And the old origin. + nsCString oldOriginStr; + rv = aMsgHdr->GetStringProperty("junkscoreorigin", oldOriginStr); + + // If this was not classified by the user, say so. + nsMsgJunkStatus oldUserClassification; + if (oldOriginStr.get()[0] != 'u') { + oldUserClassification = nsIJunkMailPlugin::UNCLASSIFIED; + } else { + // Otherwise, pass the actual user classification. + if (junkScoreStr.IsEmpty()) + oldUserClassification = nsIJunkMailPlugin::UNCLASSIFIED; + else if (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE) + oldUserClassification = nsIJunkMailPlugin::JUNK; + else + oldUserClassification = nsIJunkMailPlugin::GOOD; + + NS_ASSERTION(NS_SUCCEEDED(rv), "Converting junkScore to integer failed."); + } + + // Get the URI for this message so we can pass it to the plugin. + nsCString uri; + nsMsgKey msgKey; + nsCOMPtr<nsIMsgFolder> folder; + nsCOMPtr<nsIMsgDatabase> db; + aMsgHdr->GetMessageKey(&msgKey); + rv = aMsgHdr->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + GenerateURIForMsgKey(msgKey, folder, uri); + NS_ENSURE_SUCCESS(rv, rv); + rv = folder->GetMsgDatabase(getter_AddRefs(db)); + NS_ENSURE_SUCCESS(rv, rv); + + // Tell the plugin about this change, so that it can (potentially) + // adjust its database appropriately. + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + rv = aJunkPlugin->SetMessageClassification( + uri, oldUserClassification, aNewClassification, msgWindow, this); + NS_ENSURE_SUCCESS(rv, rv); + + // This routine is only reached if the user someone touched the UI + // and told us the junk status of this message. + // Set origin first so that listeners on the junkscore will + // know the correct origin. + rv = db->SetStringProperty(msgKey, "junkscoreorigin", "user"_ns); + NS_ASSERTION(NS_SUCCEEDED(rv), "SetStringPropertyByIndex failed"); + + // Set the junk score on the message itself. + nsAutoCString msgJunkScore; + msgJunkScore.AppendInt(aNewClassification == nsIJunkMailPlugin::JUNK + ? nsIJunkMailPlugin::IS_SPAM_SCORE + : nsIJunkMailPlugin::IS_HAM_SCORE); + db->SetStringProperty(msgKey, "junkscore", msgJunkScore); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +nsresult nsMsgDBView::GetFolderFromMsgURI(const nsACString& aMsgURI, + nsIMsgFolder** aFolder) { + NS_IF_ADDREF(*aFolder = m_folder); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnMessageClassified(const nsACString& aMsgURI, + nsMsgJunkStatus aClassification, + uint32_t aJunkPercent) + +{ + // Note: we know all messages in a batch have the same + // classification, since unlike OnMessageClassified + // methods in other classes (such as nsLocalMailFolder + // and nsImapMailFolder), this class, nsMsgDBView, currently + // only triggers message classifications due to a command to + // mark some of the messages in the view as junk, or as not + // junk - so the classification is dictated to the filter, + // not suggested by it. + // + // For this reason the only thing we (may) have to do is + // perform the action on all of the junk messages. + + uint32_t numJunk = mJunkHdrs.Length(); + NS_ASSERTION(aClassification == nsIJunkMailPlugin::GOOD || numJunk, + "the classification of a manually-marked junk message has " + "been classified as junk, yet there seem to be no such " + "outstanding messages"); + + // Is this the last message in the batch? + if (--mNumMessagesRemainingInBatch == 0 && numJunk > 0) { + PerformActionsOnJunkMsgs(aClassification == nsIJunkMailPlugin::JUNK); + mJunkHdrs.Clear(); + } + + return NS_OK; +} + +nsresult nsMsgDBView::PerformActionsOnJunkMsgs(bool msgsAreJunk) { + uint32_t numJunkHdrs = mJunkHdrs.Length(); + if (!numJunkHdrs) { + NS_ERROR("no indices of marked-as-junk messages to act on"); + return NS_OK; + } + + nsCOMPtr<nsIMsgFolder> srcFolder; + mJunkHdrs[0]->GetFolder(getter_AddRefs(srcFolder)); + + bool moveMessages, changeReadState; + nsCOMPtr<nsIMsgFolder> targetFolder; + + nsresult rv = DetermineActionsForJunkChange(msgsAreJunk, srcFolder, + moveMessages, changeReadState, + getter_AddRefs(targetFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + // Nothing to do, bail out. + if (!(moveMessages || changeReadState)) return NS_OK; + + if (changeReadState) { + // Notes on marking junk as read: + // 1. There are 2 occasions on which junk messages are marked as + // read: after a manual marking (here and in the front end) and after + // automatic classification by the bayesian filter (see code for local + // mail folders and for imap mail folders). The server-specific + // markAsReadOnSpam pref only applies to the latter, the former is + // controlled by "mailnews.ui.junk.manualMarkAsJunkMarksRead". + // 2. Even though move/delete on manual mark may be + // turned off, we might still need to mark as read. + + rv = srcFolder->MarkMessagesRead(mJunkHdrs, msgsAreJunk); + NoteChange(0, 0, nsMsgViewNotificationCode::none); + NS_ASSERTION(NS_SUCCEEDED(rv), + "marking marked-as-junk messages as read failed"); + } + + if (moveMessages) { + // Check if one of the messages to be junked is actually selected. + // If more than one message being junked, one must be selected. + // If no tree selection at all, must be in stand-alone message window. + bool junkedMsgSelected = numJunkHdrs > 1 || !mTreeSelection; + for (nsMsgViewIndex junkIndex = 0; + !junkedMsgSelected && junkIndex < numJunkHdrs; junkIndex++) { + nsMsgViewIndex hdrIndex = FindHdr(mJunkHdrs[junkIndex]); + if (hdrIndex != nsMsgViewIndex_None) + mTreeSelection->IsSelected(hdrIndex, &junkedMsgSelected); + } + + // If a junked msg is selected, tell the FE to call + // SetNextMessageAfterDelete() because a delete is coming. + if (junkedMsgSelected) { + nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater( + do_QueryReferent(mCommandUpdater)); + if (commandUpdater) { + rv = commandUpdater->UpdateNextMessageAfterDelete(); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + if (targetFolder) { + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = copyService->CopyMessages(srcFolder, mJunkHdrs, targetFolder, true, + nullptr, msgWindow, true); + } else if (msgsAreJunk) { + if (mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) { + // Unfortunately the DeleteMessages in this case is interpreted by + // IMAP as a delete toggle. So what we have to do is to assemble a + // new delete array, keeping only those that are not deleted. + nsTArray<RefPtr<nsIMsgDBHdr>> hdrsToDelete; + for (nsIMsgDBHdr* msgHdr : mJunkHdrs) { + if (msgHdr) { + uint32_t flags; + msgHdr->GetFlags(&flags); + if (!(flags & nsMsgMessageFlags::IMAPDeleted)) { + hdrsToDelete.AppendElement(msgHdr); + } + } + } + + if (!hdrsToDelete.IsEmpty()) + rv = srcFolder->DeleteMessages(hdrsToDelete, msgWindow, false, false, + nullptr, true); + } else { + rv = srcFolder->DeleteMessages(mJunkHdrs, msgWindow, false, false, + nullptr, true); + } + } else if (mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(srcFolder)); + nsTArray<nsMsgKey> imapUids(numJunkHdrs); + for (nsIMsgDBHdr* msgHdr : mJunkHdrs) { + nsMsgKey key; + msgHdr->GetMessageKey(&key); + imapUids.AppendElement(key); + } + + imapFolder->StoreImapFlags(kImapMsgDeletedFlag, false, imapUids, nullptr); + } + + NoteChange(0, 0, nsMsgViewNotificationCode::none); + + NS_ASSERTION(NS_SUCCEEDED(rv), + "move or deletion of message marked-as-junk/non junk failed"); + } + + return rv; +} + +nsresult nsMsgDBView::DetermineActionsForJunkChange( + bool msgsAreJunk, nsIMsgFolder* srcFolder, bool& moveMessages, + bool& changeReadState, nsIMsgFolder** targetFolder) { + // There are two possible actions which may be performed + // on messages marked as spam: marking as read and moving + // somewhere. When a message is marked as non junk, + // it may be moved to the inbox, and marked unread. + moveMessages = false; + changeReadState = false; + + // The 'somewhere', junkTargetFolder, can be a folder, + // but if it remains null we'll delete the messages. + *targetFolder = nullptr; + + uint32_t folderFlags; + srcFolder->GetFlags(&folderFlags); + + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = srcFolder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Handle the easy case of marking a junk message as good first. + // Set the move target folder to the inbox, if any. + if (!msgsAreJunk) { + if (folderFlags & nsMsgFolderFlags::Junk) { + prefBranch->GetBoolPref("mail.spam.markAsNotJunkMarksUnRead", + &changeReadState); + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + rv = server->GetRootMsgFolder(getter_AddRefs(rootMsgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + rootMsgFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox, targetFolder); + moveMessages = *targetFolder != nullptr; + } + + return NS_OK; + } + + nsCOMPtr<nsISpamSettings> spamSettings; + rv = server->GetSpamSettings(getter_AddRefs(spamSettings)); + NS_ENSURE_SUCCESS(rv, rv); + + // When the user explicitly marks a message as junk, we can mark it as read, + // too. This is independent of the "markAsReadOnSpam" pref, which applies + // only to automatically-classified messages. + // Note that this behaviour should match the one in the front end for marking + // as junk via toolbar/context menu. + prefBranch->GetBoolPref("mailnews.ui.junk.manualMarkAsJunkMarksRead", + &changeReadState); + + // Now let's determine whether we'll be taking the second action, + // the move / deletion (and also determine which of these two). + bool manualMark; + (void)spamSettings->GetManualMark(&manualMark); + if (!manualMark) return NS_OK; + + int32_t manualMarkMode; + (void)spamSettings->GetManualMarkMode(&manualMarkMode); + NS_ASSERTION(manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_MOVE || + manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_DELETE, + "bad manual mark mode"); + + if (manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_MOVE) { + // If this is a junk folder (not only "the" junk folder for this account) + // don't do the move. + if (folderFlags & nsMsgFolderFlags::Junk) return NS_OK; + + nsCString spamFolderURI; + rv = spamSettings->GetSpamFolderURI(spamFolderURI); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(!spamFolderURI.IsEmpty(), + "spam folder URI is empty, can't move"); + if (!spamFolderURI.IsEmpty()) { + rv = FindFolder(spamFolderURI, targetFolder); + NS_ENSURE_SUCCESS(rv, rv); + if (*targetFolder) { + moveMessages = true; + } else { + // XXX TODO: GetOrCreateJunkFolder will only create a folder with + // localized name "Junk" regardless of spamFolderURI. So if someone + // sets the junk folder to an existing folder of a different name, + // then deletes that folder, this will fail to create the correct + // folder. + rv = GetOrCreateJunkFolder(spamFolderURI, nullptr /* aListener */); + if (NS_SUCCEEDED(rv)) + rv = GetExistingFolder(spamFolderURI, targetFolder); + + NS_ASSERTION(NS_SUCCEEDED(rv), "GetOrCreateJunkFolder failed"); + } + } + + return NS_OK; + } + + // At this point manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_DELETE). + + // If this is in the trash, let's not delete. + if (folderFlags & nsMsgFolderFlags::Trash) return NS_OK; + + return srcFolder->GetCanDeleteMessages(&moveMessages); +} + +// Reversing threads involves reversing the threads but leaving the +// expanded messages ordered relative to the thread, so we +// make a copy of each array and copy them over. +void nsMsgDBView::ReverseThreads() { + nsTArray<uint32_t> newFlagArray; + nsTArray<nsMsgKey> newKeyArray; + nsTArray<uint8_t> newLevelArray; + + uint32_t viewSize = GetSize(); + uint32_t startThread = viewSize; + uint32_t nextThread = viewSize; + uint32_t destIndex = 0; + + newKeyArray.SetLength(m_keys.Length()); + newFlagArray.SetLength(m_flags.Length()); + newLevelArray.SetLength(m_levels.Length()); + + while (startThread) { + startThread--; + + if (m_flags[startThread] & MSG_VIEW_FLAG_ISTHREAD) { + for (uint32_t sourceIndex = startThread; sourceIndex < nextThread; + sourceIndex++) { + newKeyArray[destIndex] = m_keys[sourceIndex]; + newFlagArray[destIndex] = m_flags[sourceIndex]; + newLevelArray[destIndex] = m_levels[sourceIndex]; + destIndex++; + } + // Because we're copying in reverse order. + nextThread = startThread; + } + } + + m_keys.SwapElements(newKeyArray); + m_flags.SwapElements(newFlagArray); + m_levels.SwapElements(newLevelArray); +} + +void nsMsgDBView::ReverseSort() { + uint32_t topIndex = GetSize(); + + nsCOMArray<nsIMsgFolder>* folders = GetFolders(); + + // Go up half the array swapping values. + for (uint32_t bottomIndex = 0; bottomIndex < --topIndex; bottomIndex++) { + // Swap flags. + uint32_t tempFlags = m_flags[bottomIndex]; + m_flags[bottomIndex] = m_flags[topIndex]; + m_flags[topIndex] = tempFlags; + + // Swap keys. + nsMsgKey tempKey = m_keys[bottomIndex]; + m_keys[bottomIndex] = m_keys[topIndex]; + m_keys[topIndex] = tempKey; + + if (folders) { + // Swap folders -- needed when search is done across multiple folders. + nsIMsgFolder* bottomFolder = folders->ObjectAt(bottomIndex); + nsIMsgFolder* topFolder = folders->ObjectAt(topIndex); + folders->ReplaceObjectAt(topFolder, bottomIndex); + folders->ReplaceObjectAt(bottomFolder, topIndex); + } + + // No need to swap elements in m_levels; since we only call + // ReverseSort in non-threaded mode, m_levels are all the same. + } +} + +int nsMsgDBView::FnSortIdKey(const IdKey* pItem1, const IdKey* pItem2, + viewSortInfo* sortInfo) { + int32_t retVal = 0; + + nsIMsgDatabase* db = sortInfo->db; + + mozilla::DebugOnly<nsresult> rv = + db->CompareCollationKeys(pItem1->key, pItem2->key, &retVal); + NS_ASSERTION(NS_SUCCEEDED(rv), "compare failed"); + + if (retVal) return sortInfo->ascendingSort ? retVal : -retVal; + + return sortInfo->view->SecondaryCompare(pItem1->id, pItem1->folder, + pItem2->id, pItem2->folder, sortInfo); +} + +int nsMsgDBView::FnSortIdUint32(const IdUint32* pItem1, const IdUint32* pItem2, + viewSortInfo* sortInfo) { + if (pItem1->dword > pItem2->dword) { + return (sortInfo->ascendingSort) ? 1 : -1; + } + + if (pItem1->dword < pItem2->dword) { + return (sortInfo->ascendingSort) ? -1 : 1; + } + + return sortInfo->view->SecondaryCompare(pItem1->id, pItem1->folder, + pItem2->id, pItem2->folder, sortInfo); +} + +// XXX are these still correct? +// To compensate for memory alignment required for systems such as HP-UX, these +// values must be 4 bytes aligned. Don't break this when modifying the +// constants. +const int kMaxSubjectKey = 160; +const int kMaxLocationKey = 160; // Also used for account. +const int kMaxAuthorKey = 160; +const int kMaxRecipientKey = 80; + +// There are cases when pFieldType is not set: +// one case returns NS_ERROR_UNEXPECTED; +// the other case now return NS_ERROR_NULL_POINTER (this is only when +// colHandler below is null, but is very unlikely). +// The latter case used to return NS_OK, which was incorrect. +nsresult nsMsgDBView::GetFieldTypeAndLenForSort( + nsMsgViewSortTypeValue sortType, uint16_t* pMaxLen, eFieldType* pFieldType, + nsIMsgCustomColumnHandler* colHandler) { + NS_ENSURE_ARG_POINTER(pMaxLen); + NS_ENSURE_ARG_POINTER(pFieldType); + + switch (sortType) { + case nsMsgViewSortType::bySubject: + *pFieldType = kCollationKey; + *pMaxLen = kMaxSubjectKey; + break; + case nsMsgViewSortType::byAccount: + case nsMsgViewSortType::byTags: + case nsMsgViewSortType::byLocation: + *pFieldType = kCollationKey; + *pMaxLen = kMaxLocationKey; + break; + case nsMsgViewSortType::byRecipient: + case nsMsgViewSortType::byCorrespondent: + *pFieldType = kCollationKey; + *pMaxLen = kMaxRecipientKey; + break; + case nsMsgViewSortType::byAuthor: + *pFieldType = kCollationKey; + *pMaxLen = kMaxAuthorKey; + break; + case nsMsgViewSortType::byDate: + case nsMsgViewSortType::byReceived: + case nsMsgViewSortType::byPriority: + case nsMsgViewSortType::byThread: + case nsMsgViewSortType::byId: + case nsMsgViewSortType::bySize: + case nsMsgViewSortType::byFlagged: + case nsMsgViewSortType::byUnread: + case nsMsgViewSortType::byStatus: + case nsMsgViewSortType::byJunkStatus: + case nsMsgViewSortType::byAttachments: + *pFieldType = kU32; + *pMaxLen = 0; + break; + case nsMsgViewSortType::byCustom: { + if (colHandler == nullptr) { + NS_WARNING("colHandler is null. *pFieldType is not set."); + return NS_ERROR_NULL_POINTER; + } + + bool isString; + colHandler->IsString(&isString); + + if (isString) { + *pFieldType = kCollationKey; + // 80 - do we need a separate k? + *pMaxLen = kMaxRecipientKey; + } else { + *pFieldType = kU32; + *pMaxLen = 0; + } + break; + } + case nsMsgViewSortType::byNone: + // Bug 901948. + return NS_ERROR_INVALID_ARG; + default: { + nsAutoCString message("unexpected switch value: sortType="); + message.AppendInt(sortType); + NS_WARNING(message.get()); + return NS_ERROR_UNEXPECTED; + } + } + + return NS_OK; +} + +#define MSG_STATUS_MASK \ + (nsMsgMessageFlags::Replied | nsMsgMessageFlags::Forwarded) + +nsresult nsMsgDBView::GetStatusSortValue(nsIMsgDBHdr* msgHdr, + uint32_t* result) { + NS_ENSURE_ARG_POINTER(msgHdr); + NS_ENSURE_ARG_POINTER(result); + + uint32_t messageFlags; + nsresult rv = msgHdr->GetFlags(&messageFlags); + NS_ENSURE_SUCCESS(rv, rv); + + if (messageFlags & nsMsgMessageFlags::New) { + // Happily, new by definition stands alone. + *result = 0; + return NS_OK; + } + + switch (messageFlags & MSG_STATUS_MASK) { + case nsMsgMessageFlags::Replied: + *result = 2; + break; + case nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::Replied: + *result = 1; + break; + case nsMsgMessageFlags::Forwarded: + *result = 3; + break; + default: + *result = (messageFlags & nsMsgMessageFlags::Read) ? 4 : 5; + break; + } + + return NS_OK; +} + +nsresult nsMsgDBView::GetLongField(nsIMsgDBHdr* msgHdr, + nsMsgViewSortTypeValue sortType, + uint32_t* result, + nsIMsgCustomColumnHandler* colHandler) { + nsresult rv; + NS_ENSURE_ARG_POINTER(msgHdr); + NS_ENSURE_ARG_POINTER(result); + + bool isRead; + uint32_t bits; + + switch (sortType) { + case nsMsgViewSortType::bySize: + rv = (mShowSizeInLines) ? msgHdr->GetLineCount(result) + : msgHdr->GetMessageSize(result); + break; + case nsMsgViewSortType::byPriority: + nsMsgPriorityValue priority; + rv = msgHdr->GetPriority(&priority); + // Treat "none" as "normal" when sorting. + if (priority == nsMsgPriority::none) priority = nsMsgPriority::normal; + + // We want highest priority to have lowest value + // so ascending sort will have highest priority first. + *result = nsMsgPriority::highest - priority; + break; + case nsMsgViewSortType::byStatus: + rv = GetStatusSortValue(msgHdr, result); + break; + case nsMsgViewSortType::byFlagged: + bits = 0; + rv = msgHdr->GetFlags(&bits); + // Make flagged come out on top. + *result = !(bits & nsMsgMessageFlags::Marked); + break; + case nsMsgViewSortType::byUnread: + rv = msgHdr->GetIsRead(&isRead); + if (NS_SUCCEEDED(rv)) *result = !isRead; + + break; + case nsMsgViewSortType::byJunkStatus: { + nsCString junkScoreStr; + rv = msgHdr->GetStringProperty("junkscore", junkScoreStr); + // Unscored messages should come before messages that are scored + // junkScoreStr is "", and "0" - "100"; normalize to 0 - 101. + *result = junkScoreStr.IsEmpty() ? (0) : atoi(junkScoreStr.get()) + 1; + break; + } + case nsMsgViewSortType::byAttachments: + bits = 0; + rv = msgHdr->GetFlags(&bits); + *result = !(bits & nsMsgMessageFlags::Attachment); + break; + case nsMsgViewSortType::byDate: + // When sorting threads by date, we may want the date of the newest msg + // in the thread. + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay && + !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort) && + !mSortThreadsByRoot) { + nsCOMPtr<nsIMsgThread> thread; + rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv)) { + thread->GetNewestMsgDate(result); + break; + } + } + rv = msgHdr->GetDateInSeconds(result); + break; + case nsMsgViewSortType::byReceived: + // When sorting threads by received date, we may want the received date + // of the newest msg in the thread. + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay && + !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort) && + !mSortThreadsByRoot) { + nsCOMPtr<nsIMsgThread> thread; + rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread)); + NS_ENSURE_SUCCESS(rv, rv); + thread->GetNewestMsgDate(result); + } else { + // Already in seconds. + rv = msgHdr->GetUint32Property("dateReceived", result); + if (*result == 0) + // Use Date instead, we have no Received property + rv = msgHdr->GetDateInSeconds(result); + } + break; + case nsMsgViewSortType::byCustom: + if (colHandler != nullptr) { + colHandler->GetSortLongForRow(msgHdr, result); + rv = NS_OK; + } else { + NS_ASSERTION(false, + "should not be here (Sort Type: byCustom (Long), but no " + "custom handler)"); + rv = NS_ERROR_UNEXPECTED; + } + break; + case nsMsgViewSortType::byNone: + // Bug 901948. + return NS_ERROR_INVALID_ARG; + + case nsMsgViewSortType::byId: + // Handled by caller, since caller knows the key. + default: + NS_ERROR("should not be here"); + rv = NS_ERROR_UNEXPECTED; + break; + } + + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +MsgViewSortColumnInfo::MsgViewSortColumnInfo( + const MsgViewSortColumnInfo& other) { + mSortType = other.mSortType; + mSortOrder = other.mSortOrder; + mCustomColumnName = other.mCustomColumnName; + mColHandler = other.mColHandler; +} + +bool MsgViewSortColumnInfo::operator==( + const MsgViewSortColumnInfo& other) const { + return (mSortType == nsMsgViewSortType::byCustom) + ? mCustomColumnName.Equals(other.mCustomColumnName) + : mSortType == other.mSortType; +} + +nsresult nsMsgDBView::EncodeColumnSort(nsString& columnSortString) { + for (uint32_t i = 0; i < m_sortColumns.Length(); i++) { + MsgViewSortColumnInfo& sortInfo = m_sortColumns[i]; + columnSortString.Append((char)sortInfo.mSortType); + columnSortString.Append((char)sortInfo.mSortOrder + '0'); + if (sortInfo.mSortType == nsMsgViewSortType::byCustom) { + columnSortString.Append(sortInfo.mCustomColumnName); + columnSortString.Append((char16_t)'\r'); + } + } + + return NS_OK; +} + +nsresult nsMsgDBView::DecodeColumnSort(nsString& columnSortString) { + const char16_t* stringPtr = columnSortString.BeginReading(); + while (*stringPtr) { + MsgViewSortColumnInfo sortColumnInfo; + sortColumnInfo.mSortType = (nsMsgViewSortTypeValue)*stringPtr++; + sortColumnInfo.mSortOrder = (nsMsgViewSortOrderValue)(*stringPtr++) - '0'; + if (sortColumnInfo.mSortType == nsMsgViewSortType::byCustom) { + while (*stringPtr && *stringPtr != '\r') + sortColumnInfo.mCustomColumnName.Append(*stringPtr++); + + sortColumnInfo.mColHandler = + GetColumnHandler(sortColumnInfo.mCustomColumnName); + + // Advance past '\r'. + if (*stringPtr) stringPtr++; + } + + m_sortColumns.AppendElement(sortColumnInfo); + } + + return NS_OK; +} + +// Secondary Sort Key: when you select a column to sort, that +// becomes the new Primary sort key, and all previous sort keys +// become secondary. For example, if you first click on Date, +// the messages are sorted by Date; then click on From, and now the +// messages are sorted by From, and for each value of From the +// messages are in Date order. + +void nsMsgDBView::PushSort(const MsgViewSortColumnInfo& newSort) { + // Handle byNone (bug 901948) ala a mail/base/modules/DBViewerWrapper.jsm + // where we don't push the secondary sort type if it's ::byNone; + // (and secondary sort type is NOT the same as the first sort type + // there). This code should behave the same way. + + // We don't expect to be passed sort type ::byNone, + // but if we are it's safe to ignore it. + if (newSort.mSortType == nsMsgViewSortType::byNone) return; + + // byId is a unique key (misnamed as Order Received). If we are sorting byId, + // we don't need to keep any secondary sort keys. + if (newSort.mSortType == nsMsgViewSortType::byId) m_sortColumns.Clear(); + + m_sortColumns.RemoveElement(newSort); + m_sortColumns.InsertElementAt(0, newSort); + if (m_sortColumns.Length() > kMaxNumSortColumns) + m_sortColumns.RemoveElementAt(kMaxNumSortColumns); +} + +nsresult nsMsgDBView::GetCollationKey(nsIMsgDBHdr* msgHdr, + nsMsgViewSortTypeValue sortType, + nsTArray<uint8_t>& result, + nsIMsgCustomColumnHandler* colHandler) { + nsresult rv = NS_ERROR_UNEXPECTED; + NS_ENSURE_ARG_POINTER(msgHdr); + + switch (sortType) { + case nsMsgViewSortType::bySubject: + rv = msgHdr->GetSubjectCollationKey(result); + break; + case nsMsgViewSortType::byLocation: + rv = GetLocationCollationKey(msgHdr, result); + break; + case nsMsgViewSortType::byRecipient: { + nsString recipients; + rv = FetchRecipients(msgHdr, recipients); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgDatabase> dbToUse = m_db; + // Probably a search view. + if (!dbToUse) { + rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = dbToUse->CreateCollationKey(recipients, result); + } + break; + } + case nsMsgViewSortType::byAuthor: { + rv = msgHdr->GetAuthorCollationKey(result); + nsString author; + rv = FetchAuthor(msgHdr, author); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgDatabase> dbToUse = m_db; + // Probably a search view. + if (!dbToUse) { + rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = dbToUse->CreateCollationKey(author, result); + } + break; + } + case nsMsgViewSortType::byAccount: + case nsMsgViewSortType::byTags: { + nsString str; + nsCOMPtr<nsIMsgDatabase> dbToUse = m_db; + + if (!dbToUse) + // Probably a search view. + GetDBForViewIndex(0, getter_AddRefs(dbToUse)); + + rv = (sortType == nsMsgViewSortType::byAccount) + ? FetchAccount(msgHdr, str) + : FetchTags(msgHdr, str); + if (NS_SUCCEEDED(rv) && dbToUse) + rv = dbToUse->CreateCollationKey(str, result); + + break; + } + case nsMsgViewSortType::byCustom: + if (colHandler != nullptr) { + nsAutoString strKey; + rv = colHandler->GetSortStringForRow(msgHdr, strKey); + NS_ASSERTION(NS_SUCCEEDED(rv), + "failed to get sort string for custom row"); + nsAutoString strTemp(strKey); + + nsCOMPtr<nsIMsgDatabase> dbToUse = m_db; + // Probably a search view. + if (!dbToUse) { + rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = dbToUse->CreateCollationKey(strKey, result); + } else { + NS_ERROR( + "should not be here (Sort Type: byCustom (String), but no custom " + "handler)"); + rv = NS_ERROR_UNEXPECTED; + } + break; + case nsMsgViewSortType::byCorrespondent: { + nsString value; + if (IsOutgoingMsg(msgHdr)) + rv = FetchRecipients(msgHdr, value); + else + rv = FetchAuthor(msgHdr, value); + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgDatabase> dbToUse = m_db; + // Probably a search view. + if (!dbToUse) { + rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = dbToUse->CreateCollationKey(value, result); + } + break; + } + default: + rv = NS_ERROR_UNEXPECTED; + break; + } + + // Bailing out with failure will stop the sort and leave us in + // a bad state. Try to continue on, instead. + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to get the collation key"); + if (NS_FAILED(rv)) { + result.Clear(); + } + + return NS_OK; +} + +// As the location collation key is created getting folder from the msgHdr, +// it is defined in this file and not from the db. +nsresult nsMsgDBView::GetLocationCollationKey(nsIMsgDBHdr* msgHdr, + nsTArray<uint8_t>& result) { + nsCOMPtr<nsIMsgFolder> folder; + + nsresult rv = msgHdr->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgDatabase> dbToUse; + rv = folder->GetMsgDatabase(getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString locationString; + rv = folder->GetPrettyName(locationString); + NS_ENSURE_SUCCESS(rv, rv); + + return dbToUse->CreateCollationKey(locationString, result); +} + +nsresult nsMsgDBView::SaveSortInfo(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder) { + if (m_viewFolder) { + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsCOMPtr<nsIMsgDatabase> db; + nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(db)); + if (NS_SUCCEEDED(rv) && folderInfo) { + // Save off sort type and order, view type and flags. + folderInfo->SetSortType(sortType); + folderInfo->SetSortOrder(sortOrder); + + nsString sortColumnsString; + rv = EncodeColumnSort(sortColumnsString); + NS_ENSURE_SUCCESS(rv, rv); + folderInfo->SetProperty("sortColumns", sortColumnsString); + } + } + + return NS_OK; +} + +nsresult nsMsgDBView::RestoreSortInfo() { + if (!m_viewFolder) return NS_OK; + + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsCOMPtr<nsIMsgDatabase> db; + nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(db)); + if (NS_SUCCEEDED(rv) && folderInfo) { + // Restore m_sortColumns from db. + nsString sortColumnsString; + folderInfo->GetProperty("sortColumns", sortColumnsString); + DecodeColumnSort(sortColumnsString); + if (m_sortColumns.Length() > 1) { + m_secondarySort = m_sortColumns[1].mSortType; + m_secondarySortOrder = m_sortColumns[1].mSortOrder; + m_secondaryCustomColumn = m_sortColumns[1].mCustomColumnName; + } + + // Restore curCustomColumn from db. + folderInfo->GetProperty("customSortCol", m_curCustomColumn); + } + + return NS_OK; +} + +// Called by msgDBView::Sort, at which point any persisted active custom +// columns must be registered. If not, reset their m_sortColumns entries +// to byDate; Sort will fill in values if necessary based on new user sort. +void nsMsgDBView::EnsureCustomColumnsValid() { + if (!m_sortColumns.Length()) return; + + for (uint32_t i = 0; i < m_sortColumns.Length(); i++) { + if (m_sortColumns[i].mSortType == nsMsgViewSortType::byCustom && + m_sortColumns[i].mColHandler == nullptr) { + m_sortColumns[i].mSortType = nsMsgViewSortType::byDate; + m_sortColumns[i].mCustomColumnName.Truncate(); + // There are only two... + if (i == 0 && m_sortType != nsMsgViewSortType::byCustom) + SetCurCustomColumn(EmptyString()); + if (i == 1) m_secondaryCustomColumn.Truncate(); + } + } +} + +int32_t nsMsgDBView::SecondaryCompare(nsMsgKey key1, nsIMsgFolder* folder1, + nsMsgKey key2, nsIMsgFolder* folder2, + viewSortInfo* comparisonContext) { + nsMsgViewSortTypeValue sortType = comparisonContext->view->m_secondarySort; + bool isAscendingSort = comparisonContext->view->m_secondarySortOrder == + nsMsgViewSortOrder::ascending; + + // We need to make sure that in the case of the secondary sort field also + // matching, we don't recurse. + if (comparisonContext->isSecondarySort || + sortType == nsMsgViewSortType::byId) { + if (key1 > key2) { + return isAscendingSort ? 1 : -1; + } + + if (key1 < key2) { + return isAscendingSort ? -1 : 1; + } + + return 0; + } + + nsCOMPtr<nsIMsgDBHdr> hdr1, hdr2; + nsresult rv = folder1->GetMessageHeader(key1, getter_AddRefs(hdr1)); + NS_ENSURE_SUCCESS(rv, 0); + rv = folder2->GetMessageHeader(key2, getter_AddRefs(hdr2)); + NS_ENSURE_SUCCESS(rv, 0); + IdKey EntryInfo1, EntryInfo2; + + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the *secondary* sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = nullptr; + if (sortType == nsMsgViewSortType::byCustom && + comparisonContext->view->m_sortColumns.Length() > 1) { + colHandler = comparisonContext->view->m_sortColumns[1].mColHandler; + } + + // The following may leave fieldType undefined. + // In this case, we can return 0 right away since + // it is the value returned in the default case of + // switch (fieldType) statement below. + rv = GetFieldTypeAndLenForSort(sortType, &maxLen, &fieldType, colHandler); + NS_ENSURE_SUCCESS(rv, 0); + + hdr1->GetMessageKey(&EntryInfo1.id); + hdr2->GetMessageKey(&EntryInfo2.id); + + // Set up new viewSortInfo data for our secondary comparison. + viewSortInfo ctx = { + .view = comparisonContext->view, + .db = comparisonContext->db, + .isSecondarySort = true, // To avoid recursing back here! + .ascendingSort = isAscendingSort, + }; + + switch (fieldType) { + case kCollationKey: + rv = GetCollationKey(hdr1, sortType, EntryInfo1.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + rv = GetCollationKey(hdr2, sortType, EntryInfo2.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + + return FnSortIdKey(&EntryInfo1, &EntryInfo2, &ctx); + case kU32: + if (sortType == nsMsgViewSortType::byId) { + EntryInfo1.dword = EntryInfo1.id; + EntryInfo2.dword = EntryInfo2.id; + } else { + GetLongField(hdr1, sortType, &EntryInfo1.dword, colHandler); + GetLongField(hdr2, sortType, &EntryInfo2.dword, colHandler); + } + return FnSortIdUint32(&EntryInfo1, &EntryInfo2, &ctx); + default: + return 0; + } +} + +NS_IMETHODIMP nsMsgDBView::Sort(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder) { + EnsureCustomColumnsValid(); + + // If we're doing a stable sort, we can't just reverse the messages. + // Check also that the custom column we're sorting on hasn't changed. + // Otherwise, to be on the safe side, resort. + // Note: m_curCustomColumn is the desired (possibly new) custom column name, + // while m_sortColumns[0].mCustomColumnName is the name for the last completed + // sort, since these are persisted after each sort. + if (m_sortType == sortType && m_sortValid && + (sortType != nsMsgViewSortType::byCustom || + (sortType == nsMsgViewSortType::byCustom && m_sortColumns.Length() && + m_sortColumns[0].mCustomColumnName.Equals(m_curCustomColumn))) && + m_sortColumns.Length() < 2) { + // Same as it ever was. Do nothing. + if (m_sortOrder == sortOrder) return NS_OK; + + // For secondary sort, remember the sort order on a per column basis. + if (m_sortColumns.Length()) m_sortColumns[0].mSortOrder = sortOrder; + + SaveSortInfo(sortType, sortOrder); + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + ReverseThreads(); + } else { + ReverseSort(); + } + + m_sortOrder = sortOrder; + // We just reversed the sort order, we still need to invalidate the view. + return NS_OK; + } + + if (sortType == nsMsgViewSortType::byThread) return NS_OK; + + // If a sortType has changed, or the sortType is byCustom and a column has + // changed, this is the new primary sortColumnInfo. + // Note: m_curCustomColumn is the desired (possibly new) custom column name, + // while m_sortColumns[0].mCustomColumnName is the name for the last completed + // sort, since these are persisted after each sort. + if (m_sortType != sortType || + (sortType == nsMsgViewSortType::byCustom && m_sortColumns.Length() && + !m_sortColumns[0].mCustomColumnName.Equals(m_curCustomColumn))) { + // For secondary sort, remember the sort order of the original primary sort! + if (m_sortColumns.Length()) m_sortColumns[0].mSortOrder = m_sortOrder; + + MsgViewSortColumnInfo sortColumnInfo; + sortColumnInfo.mSortType = sortType; + sortColumnInfo.mSortOrder = sortOrder; + if (sortType == nsMsgViewSortType::byCustom) { + GetCurCustomColumn(sortColumnInfo.mCustomColumnName); + sortColumnInfo.mColHandler = GetCurColumnHandler(); + } + + PushSort(sortColumnInfo); + } else { + // For primary sort, remember the sort order on a per column basis. + if (m_sortColumns.Length()) m_sortColumns[0].mSortOrder = sortOrder; + } + + if (m_sortColumns.Length() > 1) { + m_secondarySort = m_sortColumns[1].mSortType; + m_secondarySortOrder = m_sortColumns[1].mSortOrder; + m_secondaryCustomColumn = m_sortColumns[1].mCustomColumnName; + } + + SaveSortInfo(sortType, sortOrder); + // Figure out how much memory we'll need, and then malloc it. + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the primary sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + + // If we did not obtain proper fieldType, it needs to be checked + // because the subsequent code does not handle it very well. + nsresult rv = + GetFieldTypeAndLenForSort(sortType, &maxLen, &fieldType, colHandler); + + // Don't sort if the field type is not supported: Bug 901948. + if (NS_FAILED(rv)) return NS_OK; + + nsTArray<void*> ptrs; + uint32_t arraySize = GetSize(); + + if (!arraySize) return NS_OK; + + nsCOMArray<nsIMsgFolder>* folders = GetFolders(); + nsCOMPtr<nsIMsgDatabase> dbToUse = m_db; + // Probably a search view. + if (!dbToUse) { + GetDBForViewIndex(0, getter_AddRefs(dbToUse)); + if (!dbToUse) return NS_ERROR_FAILURE; + } + + viewSortInfo qsPrivateData{ + .view = this, + .db = dbToUse, + .isSecondarySort = false, + .ascendingSort = (sortOrder == nsMsgViewSortOrder::ascending), + }; + + switch (fieldType) { + case kCollationKey: { + // Sort on a non-numeric field. We'll be calculating a collation key for + // each message. + nsTArray<IdKey> entries; + entries.SetLength(arraySize); + nsTArray<IdKey*> pPtrBase; + pPtrBase.SetLength(arraySize); + for (uint32_t i = 0; i < arraySize; ++i) { + IdKey* info = &entries[i]; + pPtrBase[i] = info; + info->id = m_keys[i]; + info->bits = m_flags[i]; + info->dword = 0; + info->folder = folders ? folders->ObjectAt(i) : m_folder.get(); + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr)); + NS_ASSERTION(NS_SUCCEEDED(rv) && msgHdr, "header not found"); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetCollationKey(msgHdr, sortType, info->key, colHandler); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Perform the sort. + std::sort(pPtrBase.begin(), pPtrBase.end(), + [&qsPrivateData](const auto& lhs, const auto& rhs) { + return FnSortIdKey(lhs, rhs, &qsPrivateData) < 0; + }); + + // Now update the view state to reflect the new order. + for (uint32_t i = 0; i < arraySize; ++i) { + m_keys[i] = pPtrBase[i]->id; + m_flags[i] = pPtrBase[i]->bits; + if (folders) folders->ReplaceObjectAt(pPtrBase[i]->folder, i); + } + m_sortType = sortType; + m_sortOrder = sortOrder; + m_sortValid = true; + return NS_OK; + } + case kU32: { + // Sort on a numeric field. + nsTArray<IdUint32> entries; + entries.SetLength(arraySize); + nsTArray<IdUint32*> pPtrBase; + pPtrBase.SetLength(arraySize); + for (uint32_t i = 0; i < arraySize; ++i) { + IdUint32* info = &entries[i]; + pPtrBase[i] = info; + info->id = m_keys[i]; + info->bits = m_flags[i]; + info->folder = folders ? folders->ObjectAt(i) : m_folder.get(); + if (sortType == nsMsgViewSortType::byId) { + info->dword = info->id; // No msgHdr required. + } else { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr)); + NS_ASSERTION(NS_SUCCEEDED(rv) && msgHdr, "header not found"); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetLongField(msgHdr, sortType, &info->dword, colHandler); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Perform the sort. + std::sort(pPtrBase.begin(), pPtrBase.end(), + [&qsPrivateData](const auto& lhs, const auto& rhs) { + return FnSortIdUint32(lhs, rhs, &qsPrivateData) < 0; + }); + + // Now update the view state to reflect the new order. + for (uint32_t i = 0; i < arraySize; ++i) { + m_keys[i] = pPtrBase[i]->id; + m_flags[i] = pPtrBase[i]->bits; + if (folders) folders->ReplaceObjectAt(pPtrBase[i]->folder, i); + } + m_sortType = sortType; + m_sortOrder = sortOrder; + m_sortValid = true; + return NS_OK; + } + default: + // If we get this far, we've got a bad fieldType. + return NS_ERROR_UNEXPECTED; + } +} + +nsMsgViewIndex nsMsgDBView::GetIndexOfFirstDisplayedKeyInThread( + nsIMsgThread* threadHdr, bool allowDummy) { + nsMsgViewIndex retIndex = nsMsgViewIndex_None; + uint32_t childIndex = 0; + // We could speed up the unreadOnly view by starting our search with the first + // unread message in the thread. Sometimes, that will be wrong, however, so + // let's skip it until we're sure it's necessary. + // (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) + // ? threadHdr->GetFirstUnreadKey(m_db) : threadHdr->GetChildAt(0); + uint32_t numThreadChildren; + threadHdr->GetNumChildren(&numThreadChildren); + while (retIndex == nsMsgViewIndex_None && childIndex < numThreadChildren) { + nsCOMPtr<nsIMsgDBHdr> childHdr; + threadHdr->GetChildHdrAt(childIndex++, getter_AddRefs(childHdr)); + if (childHdr) retIndex = FindHdr(childHdr, 0, allowDummy); + } + + return retIndex; +} + +nsresult nsMsgDBView::GetFirstMessageHdrToDisplayInThread( + nsIMsgThread* threadHdr, nsIMsgDBHdr** result) { + nsresult rv; + + if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) + rv = threadHdr->GetFirstUnreadChild(result); + else + rv = threadHdr->GetChildHdrAt(0, result); + + return rv; +} + +// Find the view index of the thread containing the passed msgKey, if +// the thread is in the view. MsgIndex is passed in as a shortcut if +// it turns out the msgKey is the first message in the thread, +// then we can avoid looking for the msgKey. +nsMsgViewIndex nsMsgDBView::ThreadIndexOfMsg( + nsMsgKey msgKey, nsMsgViewIndex msgIndex /* = nsMsgViewIndex_None */, + int32_t* pThreadCount /* = NULL */, uint32_t* pFlags /* = NULL */) { + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) + return nsMsgViewIndex_None; + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = m_db->GetMsgHdrForKey(msgKey, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, nsMsgViewIndex_None); + return ThreadIndexOfMsgHdr(msgHdr, msgIndex, pThreadCount, pFlags); +} + +nsMsgViewIndex nsMsgDBView::GetThreadIndex(nsMsgViewIndex msgIndex) { + if (!IsValidIndex(msgIndex)) return nsMsgViewIndex_None; + + // Scan up looking for level 0 message. + while (m_levels[msgIndex] && msgIndex) --msgIndex; + + return msgIndex; +} + +nsMsgViewIndex nsMsgDBView::ThreadIndexOfMsgHdr(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex msgIndex, + int32_t* pThreadCount, + uint32_t* pFlags) { + nsCOMPtr<nsIMsgThread> threadHdr; + nsresult rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(threadHdr)); + NS_ENSURE_SUCCESS(rv, nsMsgViewIndex_None); + + nsMsgViewIndex retIndex = nsMsgViewIndex_None; + + if (threadHdr != nullptr) { + if (msgIndex == nsMsgViewIndex_None) msgIndex = FindHdr(msgHdr, 0, true); + + // Hdr is not in view, need to find by thread. + if (msgIndex == nsMsgViewIndex_None) { + msgIndex = GetIndexOfFirstDisplayedKeyInThread(threadHdr, true); + // nsMsgKey threadKey = (msgIndex == nsMsgViewIndex_None) ? nsMsgKey_None + // : + // GetAt(msgIndex); + if (pFlags) threadHdr->GetFlags(pFlags); + } + + nsMsgViewIndex startOfThread = msgIndex; + while ((int32_t)startOfThread >= 0 && m_levels[startOfThread] != 0) + startOfThread--; + + retIndex = startOfThread; + if (pThreadCount) { + int32_t numChildren = 0; + nsMsgViewIndex threadIndex = startOfThread; + do { + threadIndex++; + numChildren++; + } while (threadIndex < m_levels.Length() && m_levels[threadIndex] != 0); + + *pThreadCount = numChildren; + } + } + + return retIndex; +} + +nsMsgKey nsMsgDBView::GetKeyOfFirstMsgInThread(nsMsgKey key) { + // Just report no key for any failure. This can occur when a + // message is deleted from a threaded view. + nsCOMPtr<nsIMsgThread> pThread; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = m_db->GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + if (NS_FAILED(rv)) return nsMsgKey_None; + + rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(pThread)); + if (NS_FAILED(rv)) return nsMsgKey_None; + + nsMsgKey firstKeyInThread = nsMsgKey_None; + + if (!pThread) return firstKeyInThread; + + // ### dmb UnreadOnly - this is wrong. But didn't seem to matter in 4.x + pThread->GetChildKeyAt(0, &firstKeyInThread); + return firstKeyInThread; +} + +NS_IMETHODIMP +nsMsgDBView::GetKeyAt(nsMsgViewIndex index, nsMsgKey* result) { + NS_ENSURE_ARG(result); + *result = GetAt(index); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetFlagsAt(nsMsgViewIndex aIndex, uint32_t* aResult) { + NS_ENSURE_ARG(aResult); + if (!IsValidIndex(aIndex)) return NS_MSG_INVALID_DBVIEW_INDEX; + + *aResult = m_flags[aIndex]; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetMsgHdrAt(nsMsgViewIndex aIndex, nsIMsgDBHdr** aResult) { + NS_ENSURE_ARG(aResult); + if (!IsValidIndex(aIndex)) return NS_MSG_INVALID_DBVIEW_INDEX; + + return GetMsgHdrForViewIndex(aIndex, aResult); +} + +nsMsgViewIndex nsMsgDBView::FindHdr(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex startIndex, + bool allowDummy) { + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + nsMsgViewIndex viewIndex = m_keys.IndexOf(msgKey, startIndex); + if (viewIndex == nsMsgViewIndex_None) return viewIndex; + + // If we're supposed to allow dummies, and the previous index is a dummy that + // is not elided, then it must be the dummy corresponding to our node and + // we should return that instead. + if (allowDummy && viewIndex && + (m_flags[viewIndex - 1] & MSG_VIEW_FLAG_DUMMY) && + !(m_flags[viewIndex - 1] & nsMsgMessageFlags::Elided)) { + viewIndex--; + } else if (!allowDummy && m_flags[viewIndex] & MSG_VIEW_FLAG_DUMMY) { + // We're not allowing dummies, and we found a dummy, look again + // one past the dummy. + return m_keys.IndexOf(msgKey, viewIndex + 1); + } + + // Check that the message we found matches the message we were looking for. + if (viewIndex != nsMsgViewIndex_None) { + nsCOMPtr<nsIMsgDBHdr> foundMsgHdr; + nsresult rv = GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(foundMsgHdr)); + if (NS_FAILED(rv) || foundMsgHdr != msgHdr) { + viewIndex = nsMsgViewIndex_None; + } + } + + return viewIndex; +} + +nsMsgViewIndex nsMsgDBView::FindKey(nsMsgKey key, bool expand) { + nsMsgViewIndex retIndex = nsMsgViewIndex_None; + retIndex = (nsMsgViewIndex)(m_keys.IndexOf(key)); + // For dummy headers, try to expand if the caller says so. And if the thread + // is expanded, ignore the dummy header and return the real header index. + if (retIndex != nsMsgViewIndex_None && + m_flags[retIndex] & MSG_VIEW_FLAG_DUMMY && + !(m_flags[retIndex] & nsMsgMessageFlags::Elided)) { + return (nsMsgViewIndex)m_keys.IndexOf(key, retIndex + 1); + } + + if (key != nsMsgKey_None && + (retIndex == nsMsgViewIndex_None || + m_flags[retIndex] & MSG_VIEW_FLAG_DUMMY) && + expand && m_db) { + nsMsgKey threadKey = GetKeyOfFirstMsgInThread(key); + if (threadKey != nsMsgKey_None) { + nsMsgViewIndex threadIndex = FindKey(threadKey, false); + if (threadIndex != nsMsgViewIndex_None) { + uint32_t flags = m_flags[threadIndex]; + if ((flags & nsMsgMessageFlags::Elided && + NS_SUCCEEDED(ExpandByIndex(threadIndex, nullptr))) || + flags & MSG_VIEW_FLAG_DUMMY) { + retIndex = (nsMsgViewIndex)m_keys.IndexOf(key, threadIndex + 1); + } + } + } + } + + return retIndex; +} + +nsresult nsMsgDBView::GetThreadCount(nsMsgViewIndex index, + uint32_t* pThreadCount) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgThread> pThread; + rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(pThread)); + if (NS_SUCCEEDED(rv) && pThread != nullptr) + rv = pThread->GetNumChildren(pThreadCount); + + return rv; +} + +// This counts the number of messages in an expanded thread, given the +// index of the first message in the thread. +int32_t nsMsgDBView::CountExpandedThread(nsMsgViewIndex index) { + int32_t numInThread = 0; + nsMsgViewIndex startOfThread = index; + while ((int32_t)startOfThread >= 0 && m_levels[startOfThread] != 0) + startOfThread--; + + nsMsgViewIndex threadIndex = startOfThread; + do { + threadIndex++; + numInThread++; + } while (threadIndex < m_levels.Length() && m_levels[threadIndex] != 0); + + return numInThread; +} + +// Returns the number of lines that would be added (> 0) or removed (< 0) +// if we were to try to expand/collapse the passed index. +nsresult nsMsgDBView::ExpansionDelta(nsMsgViewIndex index, + int32_t* expansionDelta) { + uint32_t numChildren; + nsresult rv; + + *expansionDelta = 0; + if (index >= ((nsMsgViewIndex)m_keys.Length())) + return NS_MSG_MESSAGE_NOT_FOUND; + + char flags = m_flags[index]; + + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) return NS_OK; + + // The client can pass in the key of any message + // in a thread and get the expansion delta for the thread. + + if (flags & nsMsgMessageFlags::Elided) { + rv = GetThreadCount(index, &numChildren); + NS_ENSURE_SUCCESS(rv, rv); + *expansionDelta = numChildren - 1; + } else { + numChildren = CountExpandedThread(index); + *expansionDelta = -(int32_t)(numChildren - 1); + } + + return NS_OK; +} + +nsresult nsMsgDBView::ToggleExpansion(nsMsgViewIndex index, + uint32_t* numChanged) { + nsresult rv; + NS_ENSURE_ARG(numChanged); + *numChanged = 0; + nsMsgViewIndex threadIndex = GetThreadIndex(index); + if (threadIndex == nsMsgViewIndex_None) { + NS_ASSERTION(false, "couldn't find thread"); + return NS_MSG_MESSAGE_NOT_FOUND; + } + + int32_t flags = m_flags[threadIndex]; + + // If not a thread, or doesn't have children, no expand/collapse. + // If we add sub-thread expand collapse, this will need to be relaxed. + if (!(flags & MSG_VIEW_FLAG_ISTHREAD) || !(flags & MSG_VIEW_FLAG_HASCHILDREN)) + return NS_MSG_MESSAGE_NOT_FOUND; + + if (flags & nsMsgMessageFlags::Elided) + rv = ExpandByIndex(threadIndex, numChanged); + else + rv = CollapseByIndex(threadIndex, numChanged); + + // If we collaps/uncollapse a thread, this changes the selected URIs. + SelectionChangedXPCOM(); + return rv; +} + +nsresult nsMsgDBView::ExpandAndSelectThread() { + nsresult rv; + + NS_ASSERTION(mTreeSelection, "no tree selection"); + if (!mTreeSelection) return NS_ERROR_UNEXPECTED; + + int32_t index; + rv = mTreeSelection->GetCurrentIndex(&index); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ExpandAndSelectThreadByIndex(index, false); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +nsresult nsMsgDBView::ExpandAndSelectThreadByIndex(nsMsgViewIndex index, + bool augment) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + nsresult rv; + + nsMsgViewIndex threadIndex; + bool inThreadedMode = (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay); + + if (inThreadedMode) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + threadIndex = ThreadIndexOfMsgHdr(msgHdr, index); + if (threadIndex == nsMsgViewIndex_None) { + NS_ASSERTION(false, "couldn't find thread"); + return NS_MSG_MESSAGE_NOT_FOUND; + } + } else { + threadIndex = index; + } + + int32_t flags = m_flags[threadIndex]; + int32_t count = 0; + + if (inThreadedMode && flags & MSG_VIEW_FLAG_ISTHREAD && + flags & MSG_VIEW_FLAG_HASCHILDREN) { + // If closed, expand this thread. + if (flags & nsMsgMessageFlags::Elided) { + uint32_t numExpanded; + rv = ExpandByIndex(threadIndex, &numExpanded); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Get the number of messages in the expanded thread so we know how many + // to select. + count = CountExpandedThread(threadIndex); + } else { + count = 1; + } + + NS_ASSERTION(count > 0, "bad count"); + + // Update the selection. + + NS_ASSERTION(mTreeSelection, "no tree selection"); + if (!mTreeSelection) return NS_ERROR_UNEXPECTED; + + // The count should be 1 or greater. If there was only one message in the + // thread, we just select it. If more, we select all of them. + mTreeSelection->RangedSelect(threadIndex + count - 1, threadIndex, augment); + return NS_OK; +} + +nsresult nsMsgDBView::ExpandAll() { + if (mTree) mTree->BeginUpdateBatch(); + if (mJSTree) mJSTree->BeginUpdateBatch(); + + for (int32_t i = GetSize() - 1; i >= 0; i--) { + uint32_t numExpanded; + uint32_t flags = m_flags[i]; + if (flags & nsMsgMessageFlags::Elided) ExpandByIndex(i, &numExpanded); + } + + if (mTree) mTree->EndUpdateBatch(); + if (mJSTree) mJSTree->EndUpdateBatch(); + + SelectionChangedXPCOM(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr, + nsIMsgThread** pThread) { + NS_ENSURE_ARG_POINTER(msgHdr); + NS_ENSURE_ARG_POINTER(pThread); + if (!m_db) return NS_ERROR_FAILURE; + return m_db->GetThreadContainingMsgHdr(msgHdr, pThread); +} + +nsresult nsMsgDBView::ExpandByIndex(nsMsgViewIndex index, + uint32_t* pNumExpanded) { + if ((uint32_t)index >= m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND; + + uint32_t flags = m_flags[index]; + uint32_t numExpanded = 0; + + NS_ASSERTION(flags & nsMsgMessageFlags::Elided, + "can't expand an already expanded thread"); + flags &= ~nsMsgMessageFlags::Elided; + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCOMPtr<nsIMsgThread> pThread; + nsresult rv = GetThreadContainingIndex(index, getter_AddRefs(pThread)); + NS_ENSURE_SUCCESS(rv, rv); + if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) { + // Keep top level hdr in thread, even though read. + if (flags & nsMsgMessageFlags::Read) { + m_levels.AppendElement(0); + } + + rv = ListUnreadIdsInThread(pThread, index, &numExpanded); + } else { + rv = ListIdsInThread(pThread, index, &numExpanded); + } + + if (numExpanded > 0) { + m_flags[index] = flags; + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + } + + NoteChange(index + 1, numExpanded, nsMsgViewNotificationCode::insertOrDelete); + + if (pNumExpanded != nullptr) *pNumExpanded = numExpanded; + + return rv; +} + +nsresult nsMsgDBView::CollapseAll() { + if (mJSTree) mJSTree->BeginUpdateBatch(); + for (uint32_t i = 0; i < GetSize(); i++) { + uint32_t numExpanded; + uint32_t flags = m_flags[i]; + if (!(flags & nsMsgMessageFlags::Elided) && + (flags & MSG_VIEW_FLAG_HASCHILDREN)) + CollapseByIndex(i, &numExpanded); + } + + if (mJSTree) mJSTree->EndUpdateBatch(); + SelectionChangedXPCOM(); + return NS_OK; +} + +nsresult nsMsgDBView::CollapseByIndex(nsMsgViewIndex index, + uint32_t* pNumCollapsed) { + nsresult rv; + int32_t flags = m_flags[index]; + int32_t rowDelta = 0; + + if (flags & nsMsgMessageFlags::Elided || + !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) || + !(flags & MSG_VIEW_FLAG_HASCHILDREN)) { + return NS_OK; + } + + if (index > m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND; + + rv = ExpansionDelta(index, &rowDelta); + NS_ENSURE_SUCCESS(rv, rv); + + flags |= nsMsgMessageFlags::Elided; + + m_flags[index] = flags; + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + + // Don't count first header in thread. + int32_t numRemoved = -rowDelta; + if (index + 1 + numRemoved > m_keys.Length()) { + NS_ERROR("trying to remove too many rows"); + numRemoved -= (index + 1 + numRemoved) - m_keys.Length(); + if (numRemoved <= 0) return NS_MSG_MESSAGE_NOT_FOUND; + } + + // Start at first id after thread. + RemoveRows(index + 1, numRemoved); + if (pNumCollapsed != nullptr) *pNumCollapsed = numRemoved; + + NoteChange(index + 1, rowDelta, nsMsgViewNotificationCode::insertOrDelete); + + return rv; +} + +nsresult nsMsgDBView::OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey, + bool /*ensureListed*/) { + nsresult rv = NS_OK; + // Views can override this behaviour, which is to append to view. + // This is the mail behaviour, but threaded views will want + // to insert in order... + if (newHdr) rv = AddHdr(newHdr); + + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::GetThreadContainingIndex(nsMsgViewIndex index, + nsIMsgThread** resultThread) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + return GetThreadContainingMsgHdr(msgHdr, resultThread); +} + +nsMsgViewIndex nsMsgDBView::GetIndexForThread(nsIMsgDBHdr* msgHdr) { + // Take advantage of the fact that we're already sorted + // and find the insert index via a binary search, though expanded threads + // make that tricky. + + nsMsgViewIndex highIndex = m_keys.Length(); + nsMsgViewIndex lowIndex = 0; + IdKey EntryInfo1, EntryInfo2; + + nsresult rv; + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the primary sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + + // The following may leave fieldType undefined. + // In this case, we can return highIndex right away since + // it is the value returned in the default case of + // switch (fieldType) statement below. + rv = GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler); + NS_ENSURE_SUCCESS(rv, highIndex); + + int retStatus = 0; + msgHdr->GetMessageKey(&EntryInfo1.id); + msgHdr->GetFolder(&EntryInfo1.folder); + EntryInfo1.folder->Release(); + + viewSortInfo comparisonContext{ + .view = this, + .isSecondarySort = false, + .ascendingSort = (m_sortOrder == nsMsgViewSortOrder::ascending), + }; + + nsCOMPtr<nsIMsgDatabase> hdrDB; + EntryInfo1.folder->GetMsgDatabase(getter_AddRefs(hdrDB)); + comparisonContext.db = hdrDB.get(); + switch (fieldType) { + case kCollationKey: + rv = GetCollationKey(msgHdr, m_sortType, EntryInfo1.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + break; + case kU32: + if (m_sortType == nsMsgViewSortType::byId) { + EntryInfo1.dword = EntryInfo1.id; + } else { + GetLongField(msgHdr, m_sortType, &EntryInfo1.dword, colHandler); + } + + break; + default: + return highIndex; + } + + while (highIndex > lowIndex) { + nsMsgViewIndex tryIndex = (lowIndex + highIndex) / 2; + // Need to adjust tryIndex if it's not a thread. + while (m_levels[tryIndex] && tryIndex) tryIndex--; + + if (tryIndex < lowIndex) { + NS_ERROR("try index shouldn't be less than low index"); + break; + } + + EntryInfo2.id = m_keys[tryIndex]; + GetFolderForViewIndex(tryIndex, &EntryInfo2.folder); + EntryInfo2.folder->Release(); + + nsCOMPtr<nsIMsgDBHdr> tryHdr; + nsCOMPtr<nsIMsgDatabase> db; + // ### this should get the db from the folder... + GetDBForViewIndex(tryIndex, getter_AddRefs(db)); + if (db) db->GetMsgHdrForKey(EntryInfo2.id, getter_AddRefs(tryHdr)); + + if (!tryHdr) break; + + if (tryHdr == msgHdr) { + NS_WARNING("didn't expect header to already be in view"); + highIndex = tryIndex; + break; + } + + if (fieldType == kCollationKey) { + rv = GetCollationKey(tryHdr, m_sortType, EntryInfo2.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + + retStatus = FnSortIdKey(&EntryInfo1, &EntryInfo2, &comparisonContext); + } else if (fieldType == kU32) { + if (m_sortType == nsMsgViewSortType::byId) { + EntryInfo2.dword = EntryInfo2.id; + } else { + GetLongField(tryHdr, m_sortType, &EntryInfo2.dword, colHandler); + } + + retStatus = FnSortIdUint32(&EntryInfo1, &EntryInfo2, &comparisonContext); + } + + if (retStatus == 0) { + highIndex = tryIndex; + break; + } + + if (retStatus < 0) { + highIndex = tryIndex; + // We already made sure tryIndex was at a thread at the top of the loop. + } else { + lowIndex = tryIndex + 1; + while (lowIndex < GetSize() && m_levels[lowIndex]) lowIndex++; + } + } + + return highIndex; +} + +nsMsgViewIndex nsMsgDBView::GetInsertIndexHelper( + nsIMsgDBHdr* msgHdr, nsTArray<nsMsgKey>& keys, + nsCOMArray<nsIMsgFolder>* folders, nsMsgViewSortOrderValue sortOrder, + nsMsgViewSortTypeValue sortType) { + nsMsgViewIndex highIndex = keys.Length(); + nsMsgViewIndex lowIndex = 0; + IdKey EntryInfo1, EntryInfo2; + + nsresult rv; + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the primary sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + + // The following may leave fieldType undefined. + // In this case, we can return highIndex right away since + // it is the value returned in the default case of + // switch (fieldType) statement below. + rv = GetFieldTypeAndLenForSort(sortType, &maxLen, &fieldType, colHandler); + NS_ENSURE_SUCCESS(rv, highIndex); + + int retStatus = 0; + msgHdr->GetMessageKey(&EntryInfo1.id); + msgHdr->GetFolder(&EntryInfo1.folder); + EntryInfo1.folder->Release(); + + viewSortInfo comparisonContext{ + .view = this, + .isSecondarySort = false, + .ascendingSort = (sortOrder == nsMsgViewSortOrder::ascending), + }; + + rv = EntryInfo1.folder->GetMsgDatabase(&comparisonContext.db); + NS_ENSURE_SUCCESS(rv, highIndex); + comparisonContext.db->Release(); + + switch (fieldType) { + case kCollationKey: + rv = GetCollationKey(msgHdr, sortType, EntryInfo1.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + break; + case kU32: + if (sortType == nsMsgViewSortType::byId) { + EntryInfo1.dword = EntryInfo1.id; + } else { + GetLongField(msgHdr, sortType, &EntryInfo1.dword, colHandler); + } + + break; + default: + return highIndex; + } + + while (highIndex > lowIndex) { + nsMsgViewIndex tryIndex = (lowIndex + highIndex - 1) / 2; + EntryInfo2.id = keys[tryIndex]; + EntryInfo2.folder = folders ? folders->ObjectAt(tryIndex) : m_folder.get(); + + nsCOMPtr<nsIMsgDBHdr> tryHdr; + EntryInfo2.folder->GetMessageHeader(EntryInfo2.id, getter_AddRefs(tryHdr)); + if (!tryHdr) break; + + if (fieldType == kCollationKey) { + rv = GetCollationKey(tryHdr, sortType, EntryInfo2.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + + retStatus = FnSortIdKey(&EntryInfo1, &EntryInfo2, &comparisonContext); + } else if (fieldType == kU32) { + if (sortType == nsMsgViewSortType::byId) { + EntryInfo2.dword = EntryInfo2.id; + } else { + GetLongField(tryHdr, sortType, &EntryInfo2.dword, colHandler); + } + + retStatus = FnSortIdUint32(&EntryInfo1, &EntryInfo2, &comparisonContext); + } + + if (retStatus == 0) { + highIndex = tryIndex; + break; + } + + if (retStatus < 0) { + highIndex = tryIndex; + } else { + lowIndex = tryIndex + 1; + } + } + + return highIndex; +} + +nsMsgViewIndex nsMsgDBView::GetInsertIndex(nsIMsgDBHdr* msgHdr) { + if (!GetSize()) return 0; + + if ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) != 0 && + !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort) && + m_sortOrder != nsMsgViewSortType::byId) { + return GetIndexForThread(msgHdr); + } + + return GetInsertIndexHelper(msgHdr, m_keys, GetFolders(), m_sortOrder, + m_sortType); +} + +nsresult nsMsgDBView::AddHdr(nsIMsgDBHdr* msgHdr, nsMsgViewIndex* resultIndex) { + uint32_t flags = 0; +#ifdef DEBUG_bienvenu + NS_ASSERTION(m_keys.Length() == m_flags.Length() && + (int)m_keys.Length() == m_levels.Length(), + "view arrays out of sync!"); +#endif + + if (resultIndex) *resultIndex = nsMsgViewIndex_None; + + if (!GetShowingIgnored()) { + nsCOMPtr<nsIMsgThread> thread; + GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread)); + if (thread) { + thread->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Ignored) return NS_OK; + } + + bool ignored; + msgHdr->GetIsKilled(&ignored); + if (ignored) return NS_OK; + } + + nsMsgKey msgKey, threadId; + nsMsgKey threadParent; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetThreadId(&threadId); + msgHdr->GetThreadParent(&threadParent); + + msgHdr->GetFlags(&flags); + // XXX this isn't quite right, is it? + // Should be checking that our thread parent key is none? + if (threadParent == nsMsgKey_None) flags |= MSG_VIEW_FLAG_ISTHREAD; + + nsMsgViewIndex insertIndex = GetInsertIndex(msgHdr); + if (insertIndex == nsMsgViewIndex_None) { + // If unreadonly, level is 0 because we must be the only msg in the thread. + int32_t levelToAdd = 0; + + if (m_sortOrder == nsMsgViewSortOrder::ascending) { + InsertMsgHdrAt(GetSize(), msgHdr, msgKey, flags, levelToAdd); + if (resultIndex) *resultIndex = GetSize() - 1; + + // The call to NoteChange() has to happen after we add the key as + // NoteChange() will call RowCountChanged() which will call our + // GetRowCount(). + NoteChange(GetSize() - 1, 1, nsMsgViewNotificationCode::insertOrDelete); + } else { + InsertMsgHdrAt(0, msgHdr, msgKey, flags, levelToAdd); + if (resultIndex) *resultIndex = 0; + + // The call to NoteChange() has to happen after we insert the key as + // NoteChange() will call RowCountChanged() which will call our + // GetRowCount(). + NoteChange(0, 1, nsMsgViewNotificationCode::insertOrDelete); + } + + m_sortValid = false; + } else { + InsertMsgHdrAt(insertIndex, msgHdr, msgKey, flags, 0); + if (resultIndex) *resultIndex = insertIndex; + + // The call to NoteChange() has to happen after we add the key as + // NoteChange() will call RowCountChanged() which will call our + // GetRowCount(). + NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete); + } + + OnHeaderAddedOrDeleted(); + return NS_OK; +} + +bool nsMsgDBView::WantsThisThread(nsIMsgThread* /*threadHdr*/) { + // Default is to want all threads. + return true; +} + +nsMsgViewIndex nsMsgDBView::FindParentInThread( + nsMsgKey parentKey, nsMsgViewIndex startOfThreadViewIndex) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + while (parentKey != nsMsgKey_None) { + nsMsgViewIndex parentIndex = + m_keys.IndexOf(parentKey, startOfThreadViewIndex); + if (parentIndex != nsMsgViewIndex_None) return parentIndex; + + if (NS_FAILED(m_db->GetMsgHdrForKey(parentKey, getter_AddRefs(msgHdr)))) + break; + + msgHdr->GetThreadParent(&parentKey); + } + + return startOfThreadViewIndex; +} + +nsresult nsMsgDBView::ListIdsInThreadOrder(nsIMsgThread* threadHdr, + nsMsgKey parentKey, uint32_t level, + nsMsgViewIndex* viewIndex, + uint32_t* pNumListed) { + nsCOMPtr<nsIMsgEnumerator> msgEnumerator; + nsresult rv = + threadHdr->EnumerateMessages(parentKey, getter_AddRefs(msgEnumerator)); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t numChildren; + (void)threadHdr->GetNumChildren(&numChildren); + NS_ASSERTION(numChildren, "Empty thread in view/db"); + // Bogus, but harmless. + if (!numChildren) return NS_OK; + + // Account for the existing thread root. + numChildren--; + + // Skip the first one. + bool hasMore; + while (NS_SUCCEEDED(msgEnumerator->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = msgEnumerator->GetNext(getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + if (*pNumListed == numChildren) { + MOZ_ASSERT_UNREACHABLE("thread corrupt in db"); + // If we've listed more messages than are in the thread, then the db + // is corrupt, and we should invalidate it. + // We'll use this rv to indicate there's something wrong with the db + // though for now it probably won't get paid attention to. + m_db->SetSummaryValid(false); + return NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + } + + if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) { + bool ignored; + msgHdr->GetIsKilled(&ignored); + // We are not going to process subthreads, horribly invalidating the + // numChildren characteristic. + if (ignored) continue; + } + + nsMsgKey msgKey; + uint32_t msgFlags, newFlags; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFlags(&msgFlags); + AdjustReadFlag(msgHdr, &msgFlags); + SetMsgHdrAt(msgHdr, *viewIndex, msgKey, msgFlags & ~MSG_VIEW_FLAGS, level); + // Turn off thread or elided bit if they got turned on (maybe from new + // only view?) + msgHdr->AndFlags(~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided), + &newFlags); + (*pNumListed)++; + (*viewIndex)++; + rv = ListIdsInThreadOrder(threadHdr, msgKey, level + 1, viewIndex, + pNumListed); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +void nsMsgDBView::InsertEmptyRows(nsMsgViewIndex viewIndex, int32_t numRows) { + m_keys.InsertElementsAt(viewIndex, numRows, 0); + m_flags.InsertElementsAt(viewIndex, numRows, 0); + m_levels.InsertElementsAt(viewIndex, numRows, 1); +} + +void nsMsgDBView::RemoveRows(nsMsgViewIndex viewIndex, int32_t numRows) { + m_keys.RemoveElementsAt(viewIndex, numRows); + m_flags.RemoveElementsAt(viewIndex, numRows); + m_levels.RemoveElementsAt(viewIndex, numRows); +} + +NS_IMETHODIMP +nsMsgDBView::InsertTreeRows(nsMsgViewIndex aIndex, uint32_t aNumRows, + nsMsgKey aKey, nsMsgViewFlagsTypeValue aFlags, + uint32_t aLevel, nsIMsgFolder* aFolder) { + if (GetSize() < aIndex) return NS_ERROR_UNEXPECTED; + + nsCOMArray<nsIMsgFolder>* folders = GetFolders(); + if (folders) { + // In a search/xfvf view only, a folder is required. + NS_ENSURE_ARG_POINTER(aFolder); + for (size_t i = 0; i < aNumRows; i++) + // Insert into m_folders. + if (!folders->InsertObjectAt(aFolder, aIndex + i)) + return NS_ERROR_UNEXPECTED; + } + + m_keys.InsertElementsAt(aIndex, aNumRows, aKey); + m_flags.InsertElementsAt(aIndex, aNumRows, aFlags); + m_levels.InsertElementsAt(aIndex, aNumRows, aLevel); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::RemoveTreeRows(nsMsgViewIndex aIndex, uint32_t aNumRows) { + // Prevent a crash if attempting to remove rows which don't exist. + if (GetSize() < aIndex + aNumRows) return NS_ERROR_UNEXPECTED; + + nsMsgDBView::RemoveRows(aIndex, aNumRows); + + nsCOMArray<nsIMsgFolder>* folders = GetFolders(); + if (folders) + // In a search/xfvf view only, remove from m_folders. + if (!folders->RemoveObjectsAt(aIndex, aNumRows)) return NS_ERROR_UNEXPECTED; + + return NS_OK; +} + +nsresult nsMsgDBView::ListIdsInThread(nsIMsgThread* threadHdr, + nsMsgViewIndex startOfThreadViewIndex, + uint32_t* pNumListed) { + NS_ENSURE_ARG(threadHdr); + // These children ids should be in thread order. + nsresult rv = NS_OK; + uint32_t i; + nsMsgViewIndex viewIndex = startOfThreadViewIndex + 1; + *pNumListed = 0; + + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + NS_ASSERTION(numChildren, "Empty thread in view/db"); + if (!numChildren) return NS_OK; + + // Account for the existing thread root. + numChildren--; + InsertEmptyRows(viewIndex, numChildren); + + // ### need to rework this when we implemented threading in group views. + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay && + !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) { + nsMsgKey parentKey = m_keys[startOfThreadViewIndex]; + // If the thread is bigger than the hdr cache, expanding the thread + // can be slow. Increasing the hdr cache size will help a fair amount. + uint32_t hdrCacheSize; + m_db->GetMsgHdrCacheSize(&hdrCacheSize); + if (numChildren > hdrCacheSize) m_db->SetMsgHdrCacheSize(numChildren); + + // If this fails, *pNumListed will be 0, and we'll fall back to just + // enumerating the messages in the thread below. + rv = ListIdsInThreadOrder(threadHdr, parentKey, 1, &viewIndex, pNumListed); + if (numChildren > hdrCacheSize) m_db->SetMsgHdrCacheSize(hdrCacheSize); + } + + if (!*pNumListed) { + uint32_t ignoredHeaders = 0; + // If we're not threaded, just list em out in db order. + for (i = 1; i <= numChildren; i++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr)); + + if (msgHdr != nullptr) { + if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) { + bool killed; + msgHdr->GetIsKilled(&killed); + if (killed) { + ignoredHeaders++; + continue; + } + } + + nsMsgKey msgKey; + uint32_t msgFlags, newFlags; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFlags(&msgFlags); + AdjustReadFlag(msgHdr, &msgFlags); + SetMsgHdrAt(msgHdr, viewIndex, msgKey, msgFlags & ~MSG_VIEW_FLAGS, 1); + // Here, we're either flat, or we're grouped - in either case, + // level is 1. Turn off thread or elided bit if they got turned on + // (maybe from new only view?). + if (i > 0) + msgHdr->AndFlags( + ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided), &newFlags); + + (*pNumListed)++; + viewIndex++; + } + } + + if (ignoredHeaders + *pNumListed < numChildren) { + MOZ_ASSERT_UNREACHABLE("thread corrupt in db"); + // If we've listed fewer messages than are in the thread, then the db + // is corrupt, and we should invalidate it. + // We'll use this rv to indicate there's something wrong with the db + // though for now it probably won't get paid attention to. + m_db->SetSummaryValid(false); + rv = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + } + } + + // We may have added too many elements (i.e., subthreads were cut). + // XXX Fix for cross folder view case. + if (*pNumListed < numChildren) + RemoveRows(viewIndex, numChildren - *pNumListed); + + return rv; +} + +int32_t nsMsgDBView::FindLevelInThread(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex startOfThread, + nsMsgViewIndex viewIndex) { + nsCOMPtr<nsIMsgDBHdr> curMsgHdr = msgHdr; + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + + // Look through the ancestors of the passed in msgHdr in turn, looking for + // them in the view, up to the start of the thread. If we find an ancestor, + // then our level is one greater than the level of the ancestor. + while (curMsgHdr) { + nsMsgKey parentKey; + curMsgHdr->GetThreadParent(&parentKey); + if (parentKey == nsMsgKey_None) break; + + // Scan up to find view index of ancestor, if any. + for (nsMsgViewIndex indexToTry = viewIndex; + indexToTry && indexToTry-- >= startOfThread;) { + if (m_keys[indexToTry] == parentKey) return m_levels[indexToTry] + 1; + } + + // If msgHdr's key is its parentKey, we'll loop forever, so protect + // against that corruption. + if (msgKey == parentKey || NS_FAILED(m_db->GetMsgHdrForKey( + parentKey, getter_AddRefs(curMsgHdr)))) { + NS_ERROR( + "msgKey == parentKey, or GetMsgHdrForKey failed, this used to be an " + "infinite loop condition"); + curMsgHdr = nullptr; + } else { + // Need to update msgKey so the check for a msgHdr with matching + // key+parentKey will work after first time through loop. + curMsgHdr->GetMessageKey(&msgKey); + } + } + + return 1; +} + +// XXX Can this be combined with GetIndexForThread?? +nsMsgViewIndex nsMsgDBView::GetThreadRootIndex(nsIMsgDBHdr* msgHdr) { + if (!msgHdr) { + NS_WARNING("null msgHdr parameter"); + return nsMsgViewIndex_None; + } + + // Take advantage of the fact that we're already sorted + // and find the thread root via a binary search. + + nsMsgViewIndex highIndex = m_keys.Length(); + nsMsgViewIndex lowIndex = 0; + IdKey EntryInfo1, EntryInfo2; + + nsresult rv; + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the primary sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + + // The following may leave fieldType undefined. + // In this case, we can return highIndex right away since + // it is the value returned in the default case of + // switch (fieldType) statement below. + rv = GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler); + NS_ENSURE_SUCCESS(rv, highIndex); + + int retStatus = 0; + msgHdr->GetMessageKey(&EntryInfo1.id); + msgHdr->GetFolder(&EntryInfo1.folder); + EntryInfo1.folder->Release(); + + viewSortInfo comparisonContext{ + .view = this, + .isSecondarySort = false, + .ascendingSort = (m_sortOrder == nsMsgViewSortOrder::ascending), + }; + + nsCOMPtr<nsIMsgDatabase> hdrDB; + EntryInfo1.folder->GetMsgDatabase(getter_AddRefs(hdrDB)); + comparisonContext.db = hdrDB.get(); + + switch (fieldType) { + case kCollationKey: + rv = GetCollationKey(msgHdr, m_sortType, EntryInfo1.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + break; + case kU32: + if (m_sortType == nsMsgViewSortType::byId) { + EntryInfo1.dword = EntryInfo1.id; + } else { + GetLongField(msgHdr, m_sortType, &EntryInfo1.dword, colHandler); + } + + break; + default: + return highIndex; + } + + while (highIndex > lowIndex) { + nsMsgViewIndex tryIndex = (lowIndex + highIndex) / 2; + // Need to adjust tryIndex if it's not a thread. + while (m_levels[tryIndex] && tryIndex) tryIndex--; + + if (tryIndex < lowIndex) { + NS_ERROR("try index shouldn't be less than low index"); + break; + } + + EntryInfo2.id = m_keys[tryIndex]; + GetFolderForViewIndex(tryIndex, &EntryInfo2.folder); + EntryInfo2.folder->Release(); + + nsCOMPtr<nsIMsgDBHdr> tryHdr; + nsCOMPtr<nsIMsgDatabase> db; + // ### this should get the db from the folder... + GetDBForViewIndex(tryIndex, getter_AddRefs(db)); + if (db) db->GetMsgHdrForKey(EntryInfo2.id, getter_AddRefs(tryHdr)); + + if (!tryHdr) break; + + if (tryHdr == msgHdr) { + highIndex = tryIndex; + break; + } + + if (fieldType == kCollationKey) { + rv = GetCollationKey(tryHdr, m_sortType, EntryInfo2.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + retStatus = FnSortIdKey(&EntryInfo1, &EntryInfo2, &comparisonContext); + } else if (fieldType == kU32) { + if (m_sortType == nsMsgViewSortType::byId) { + EntryInfo2.dword = EntryInfo2.id; + } else { + GetLongField(tryHdr, m_sortType, &EntryInfo2.dword, colHandler); + } + + retStatus = FnSortIdUint32(&EntryInfo1, &EntryInfo2, &comparisonContext); + } + + if (retStatus == 0) { + highIndex = tryIndex; + break; + } + + if (retStatus < 0) { + highIndex = tryIndex; + // We already made sure tryIndex was at a thread at the top of the loop. + } else { + lowIndex = tryIndex + 1; + while (lowIndex < GetSize() && m_levels[lowIndex]) lowIndex++; + } + } + + nsCOMPtr<nsIMsgDBHdr> resultHdr; + GetMsgHdrForViewIndex(highIndex, getter_AddRefs(resultHdr)); + + if (resultHdr != msgHdr) { + NS_WARNING("didn't find hdr"); + highIndex = FindHdr(msgHdr); +#ifdef DEBUG_David_Bienvenu + if (highIndex != nsMsgViewIndex_None) { + NS_WARNING("but find hdr did"); + printf("level of found hdr = %d\n", m_levels[highIndex]); + ValidateSort(); + } +#endif + return highIndex; + } + + return msgHdr == resultHdr ? highIndex : nsMsgViewIndex_None; +} + +#ifdef DEBUG_David_Bienvenu + +void nsMsgDBView::InitEntryInfoForIndex(nsMsgViewIndex i, IdKey& EntryInfo) { + nsresult rv; + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the primary sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + + // The following may leave fieldType undefined. + rv = GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to obtain fieldType"); + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr)); + + msgHdr->GetMessageKey(&EntryInfo.id); + msgHdr->GetFolder(&EntryInfo.folder); + EntryInfo.folder->Release(); + + nsCOMPtr<nsIMsgDatabase> hdrDB; + EntryInfo.folder->GetMsgDatabase(getter_AddRefs(hdrDB)); + switch (fieldType) { + case kCollationKey: + rv = GetCollationKey(msgHdr, m_sortType, EntryInfo.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + break; + case kU32: + if (m_sortType == nsMsgViewSortType::byId) + EntryInfo.dword = EntryInfo.id; + else + GetLongField(msgHdr, m_sortType, &EntryInfo.dword, colHandler); + + break; + default: + NS_ERROR("invalid field type"); + } +} + +void nsMsgDBView::ValidateSort() { + IdKey EntryInfo1, EntryInfo2; + nsCOMPtr<nsIMsgDBHdr> hdr1, hdr2; + + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the primary sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + + // It is not entirely clear what we should do since, + // if fieldType is not available, there is no way to know + // how to compare the field to check for sorting. + // So we bomb out here. It is OK since this is debug code + // inside #ifdef DEBUG_David_Bienvenu + nsresult rv = + GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to obtain fieldType"); + + viewSortInfo comparisonContext; + comparisonContext.view = this; + comparisonContext.isSecondarySort = false; + comparisonContext.ascendingSort = + (m_sortOrder == nsMsgViewSortOrder::ascending); + nsCOMPtr<nsIMsgDatabase> db; + GetDBForViewIndex(0, getter_AddRefs(db)); + // This is only for comparing collation keys - it could be any db. + comparisonContext.db = db.get(); + + for (nsMsgViewIndex i = 0; i < m_keys.Length();) { + // Ignore non threads. + if (m_levels[i]) { + i++; + continue; + } + + // Find next header. + nsMsgViewIndex j = i + 1; + while (j < m_keys.Length() && m_levels[j]) j++; + + if (j == m_keys.Length()) break; + + InitEntryInfoForIndex(i, EntryInfo1); + InitEntryInfoForIndex(j, EntryInfo2); + const void *pValue1 = &EntryInfo1, *pValue2 = &EntryInfo2; + int retStatus = 0; + if (fieldType == kCollationKey) + retStatus = FnSortIdKey(&pValue1, &pValue2, &comparisonContext); + else if (fieldType == kU32) + retStatus = FnSortIdUint32(&pValue1, &pValue2, &comparisonContext); + + if (retStatus && + (retStatus < 0) == (m_sortOrder == nsMsgViewSortOrder::ascending)) { + NS_ERROR("view not sorted correctly"); + break; + } + // j is the new i. + i = j; + } +} + +#endif + +nsresult nsMsgDBView::ListUnreadIdsInThread( + nsIMsgThread* threadHdr, nsMsgViewIndex startOfThreadViewIndex, + uint32_t* pNumListed) { + NS_ENSURE_ARG(threadHdr); + // These children ids should be in thread order. + nsMsgViewIndex viewIndex = startOfThreadViewIndex + 1; + *pNumListed = 0; + nsMsgKey topLevelMsgKey = m_keys[startOfThreadViewIndex]; + + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + uint32_t i; + for (i = 0; i < numChildren; i++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr)); + if (msgHdr != nullptr) { + if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) { + bool killed; + msgHdr->GetIsKilled(&killed); + if (killed) continue; + } + + nsMsgKey msgKey; + uint32_t msgFlags; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFlags(&msgFlags); + bool isRead = AdjustReadFlag(msgHdr, &msgFlags); + if (!isRead) { + // Just make sure flag is right in db. + m_db->MarkHdrRead(msgHdr, false, nullptr); + if (msgKey != topLevelMsgKey) { + InsertMsgHdrAt( + viewIndex, msgHdr, msgKey, msgFlags, + FindLevelInThread(msgHdr, startOfThreadViewIndex, viewIndex)); + viewIndex++; + (*pNumListed)++; + } + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, + uint32_t aNewFlags, + nsIDBChangeListener* aInstigator) { + // If we're not the instigator, update flags if this key is in our view. + if (aInstigator != this) { + NS_ENSURE_ARG_POINTER(aHdrChanged); + nsMsgKey msgKey; + aHdrChanged->GetMessageKey(&msgKey); + nsMsgViewIndex index = FindHdr(aHdrChanged); + if (index != nsMsgViewIndex_None) { + uint32_t viewOnlyFlags = + m_flags[index] & (MSG_VIEW_FLAGS | nsMsgMessageFlags::Elided); + + // XXX what about saving the old view only flags, like IsThread and + // HasChildren? + // I think we'll want to save those away. + m_flags[index] = aNewFlags | viewOnlyFlags; + // Tell the view the extra flag changed, so it can + // update the previous view, if any. + OnExtraFlagChanged(index, aNewFlags); + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + } + + uint32_t deltaFlags = (aOldFlags ^ aNewFlags); + if (deltaFlags & (nsMsgMessageFlags::Read | nsMsgMessageFlags::New)) { + nsMsgViewIndex threadIndex = + ThreadIndexOfMsgHdr(aHdrChanged, index, nullptr, nullptr); + + // May need to fix thread counts. + if (threadIndex != nsMsgViewIndex_None && threadIndex != index) + NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed); + } + } + + // Don't need to propagate notifications, right? + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnHdrDeleted(nsIMsgDBHdr* aHdrChanged, nsMsgKey aParentKey, + int32_t aFlags, nsIDBChangeListener* aInstigator) { + nsMsgViewIndex deletedIndex = FindHdr(aHdrChanged); + if (IsValidIndex(deletedIndex)) { + // Check if this message is currently selected. If it is, tell the frontend + // to be prepared for a delete. + nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater( + do_QueryReferent(mCommandUpdater)); + bool isMsgSelected = false; + if (mTreeSelection && commandUpdater) { + mTreeSelection->IsSelected(deletedIndex, &isMsgSelected); + if (isMsgSelected) commandUpdater->UpdateNextMessageAfterDelete(); + } + + RemoveByIndex(deletedIndex); + + if (isMsgSelected) { + // Now tell the front end that the delete happened. + commandUpdater->SelectedMessageRemoved(); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnHdrAdded(nsIMsgDBHdr* aHdrChanged, nsMsgKey aParentKey, + int32_t aFlags, nsIDBChangeListener* aInstigator) { + return OnNewHeader(aHdrChanged, aParentKey, false); + // Probably also want to pass that parent key in, since we went to the + // trouble of figuring out what it is. +} + +NS_IMETHODIMP +nsMsgDBView::OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange, + const nsACString& property, bool aPreChange, + uint32_t* aStatus, + nsIDBChangeListener* aInstigator) { + if (aPreChange) return NS_OK; + + if (aHdrToChange) { + nsMsgViewIndex index = FindHdr(aHdrToChange); + if (index != nsMsgViewIndex_None) + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnParentChanged(nsMsgKey aKeyChanged, nsMsgKey oldParent, + nsMsgKey newParent, + nsIDBChangeListener* aInstigator) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnAnnouncerGoingAway(nsIDBChangeAnnouncer* instigator) { + if (m_db) { + m_db->RemoveListener(this); + m_db = nullptr; + } + + int32_t saveSize = GetSize(); + ClearHdrCache(); + + // This is important, because the tree will ask us for our + // row count, which get determine from the number of keys. + m_keys.Clear(); + // Be consistent. + m_flags.Clear(); + m_levels.Clear(); + + // Tell the tree all the rows have gone away. + if (mTree) mTree->RowCountChanged(0, -saveSize); + if (mJSTree) mJSTree->RowCountChanged(0, -saveSize); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnEvent(nsIMsgDatabase* aDB, const char* aEvent) { + if (!strcmp(aEvent, "DBOpened")) m_db = aDB; + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnReadChanged(nsIDBChangeListener* aInstigator) { return NS_OK; } + +NS_IMETHODIMP +nsMsgDBView::OnJunkScoreChanged(nsIDBChangeListener* aInstigator) { + return NS_OK; +} + +void nsMsgDBView::ClearHdrCache() { + m_cachedHdr = nullptr; + m_cachedMsgKey = nsMsgKey_None; +} + +NS_IMETHODIMP +nsMsgDBView::SetSuppressChangeNotifications(bool aSuppressChangeNotifications) { + mSuppressChangeNotification = aSuppressChangeNotifications; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSuppressChangeNotifications( + bool* aSuppressChangeNotifications) { + NS_ENSURE_ARG_POINTER(aSuppressChangeNotifications); + *aSuppressChangeNotifications = mSuppressChangeNotification; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::NoteChange(nsMsgViewIndex firstLineChanged, int32_t numChanged, + nsMsgViewNotificationCodeValue changeType) { + if ((mTree || mJSTree) && !mSuppressChangeNotification) { + switch (changeType) { + case nsMsgViewNotificationCode::changed: + if (mTree) + mTree->InvalidateRange(firstLineChanged, + firstLineChanged + numChanged - 1); + if (mJSTree) + mJSTree->InvalidateRange(firstLineChanged, + firstLineChanged + numChanged - 1); + break; + case nsMsgViewNotificationCode::insertOrDelete: + if (numChanged < 0) mRemovingRow = true; + + // The caller needs to have adjusted m_keys before getting here, since + // RowCountChanged() will call our GetRowCount(). + if (mTree) mTree->RowCountChanged(firstLineChanged, numChanged); + if (mJSTree) mJSTree->RowCountChanged(firstLineChanged, numChanged); + mRemovingRow = false; + [[fallthrough]]; + case nsMsgViewNotificationCode::all: + ClearHdrCache(); + break; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSortOrder(nsMsgViewSortOrderValue* aSortOrder) { + NS_ENSURE_ARG_POINTER(aSortOrder); + *aSortOrder = m_sortOrder; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSortType(nsMsgViewSortTypeValue* aSortType) { + NS_ENSURE_ARG_POINTER(aSortType); + *aSortType = m_sortType; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetSortType(nsMsgViewSortTypeValue aSortType) { + m_sortType = aSortType; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetViewType(nsMsgViewTypeValue* aViewType) { + NS_ERROR("you should be overriding this"); + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +nsMsgDBView::GetSecondarySortOrder(nsMsgViewSortOrderValue* aSortOrder) { + NS_ENSURE_ARG_POINTER(aSortOrder); + *aSortOrder = m_secondarySortOrder; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetSecondarySortOrder(nsMsgViewSortOrderValue aSortOrder) { + m_secondarySortOrder = aSortOrder; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSecondarySortType(nsMsgViewSortTypeValue* aSortType) { + NS_ENSURE_ARG_POINTER(aSortType); + *aSortType = m_secondarySort; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetSecondarySortType(nsMsgViewSortTypeValue aSortType) { + m_secondarySort = aSortType; + return NS_OK; +} + +nsresult nsMsgDBView::PersistFolderInfo(nsIDBFolderInfo** dbFolderInfo) { + nsresult rv = m_db->GetDBFolderInfo(dbFolderInfo); + NS_ENSURE_SUCCESS(rv, rv); + // Save off sort type and order, view type and flags. + (*dbFolderInfo)->SetSortType(m_sortType); + (*dbFolderInfo)->SetSortOrder(m_sortOrder); + (*dbFolderInfo)->SetViewFlags(m_viewFlags); + nsMsgViewTypeValue viewType; + GetViewType(&viewType); + (*dbFolderInfo)->SetViewType(viewType); + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::GetViewFlags(nsMsgViewFlagsTypeValue* aViewFlags) { + NS_ENSURE_ARG_POINTER(aViewFlags); + *aViewFlags = m_viewFlags; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) { + // If we're turning off threaded display, we need to expand all so that all + // messages will be displayed. + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay && + !(aViewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { + ExpandAll(); + // Invalidate the sort so sorting will do something. + m_sortValid = false; + } + + m_viewFlags = aViewFlags; + + if (m_viewFolder) { + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(db)); + NS_ENSURE_SUCCESS(rv, rv); + return folderInfo->SetViewFlags(aViewFlags); + } else + return NS_OK; +} + +nsresult nsMsgDBView::MarkThreadOfMsgRead(nsMsgKey msgId, + nsMsgViewIndex msgIndex, + nsTArray<nsMsgKey>& idsMarkedRead, + bool bRead) { + nsCOMPtr<nsIMsgThread> threadHdr; + nsresult rv = GetThreadContainingIndex(msgIndex, getter_AddRefs(threadHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + nsMsgViewIndex threadIndex; + + NS_ASSERTION(threadHdr, "threadHdr is null"); + if (!threadHdr) return NS_MSG_MESSAGE_NOT_FOUND; + + nsCOMPtr<nsIMsgDBHdr> firstHdr; + rv = threadHdr->GetChildHdrAt(0, getter_AddRefs(firstHdr)); + NS_ENSURE_SUCCESS(rv, rv); + nsMsgKey firstHdrId; + firstHdr->GetMessageKey(&firstHdrId); + if (msgId != firstHdrId) + threadIndex = GetIndexOfFirstDisplayedKeyInThread(threadHdr); + else + threadIndex = msgIndex; + + return MarkThreadRead(threadHdr, threadIndex, idsMarkedRead, bRead); +} + +nsresult nsMsgDBView::MarkThreadRead(nsIMsgThread* threadHdr, + nsMsgViewIndex threadIndex, + nsTArray<nsMsgKey>& idsMarkedRead, + bool bRead) { + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + idsMarkedRead.SetCapacity(numChildren); + for (int32_t childIndex = 0; childIndex < (int32_t)numChildren; + childIndex++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + threadHdr->GetChildHdrAt(childIndex, getter_AddRefs(msgHdr)); + NS_ASSERTION(msgHdr, "msgHdr is null"); + if (!msgHdr) continue; + + bool isRead; + nsMsgKey hdrMsgId; + msgHdr->GetMessageKey(&hdrMsgId); + nsCOMPtr<nsIMsgDatabase> db; + nsresult rv = GetDBForHeader(msgHdr, getter_AddRefs(db)); + NS_ENSURE_SUCCESS(rv, rv); + db->IsRead(hdrMsgId, &isRead); + + if (isRead != bRead) { + // MarkHdrRead will change the unread count on the thread. + db->MarkHdrRead(msgHdr, bRead, nullptr); + // Insert at the front. Should we insert at the end? + idsMarkedRead.InsertElementAt(0, hdrMsgId); + } + } + + return NS_OK; +} + +bool nsMsgDBView::AdjustReadFlag(nsIMsgDBHdr* msgHdr, uint32_t* msgFlags) { + // If we're a cross-folder view, just bail on this. + if (GetFolders()) return *msgFlags & nsMsgMessageFlags::Read; + + bool isRead = false; + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + m_db->IsRead(msgKey, &isRead); + // Just make sure flag is right in db. +#ifdef DEBUG_David_Bienvenu + NS_ASSERTION(isRead == ((*msgFlags & nsMsgMessageFlags::Read) != 0), + "msgFlags out of sync"); +#endif + if (isRead) + *msgFlags |= nsMsgMessageFlags::Read; + else + *msgFlags &= ~nsMsgMessageFlags::Read; + + m_db->MarkHdrRead(msgHdr, isRead, nullptr); + return isRead; +} + +// Starting from startIndex, performs the passed in navigation, including +// any marking read needed, and returns the resultId and resultIndex of the +// destination of the navigation. If no message is found in the view, +// it returns a resultId of nsMsgKey_None and an resultIndex of +// nsMsgViewIndex_None. +NS_IMETHODIMP +nsMsgDBView::ViewNavigate(nsMsgNavigationTypeValue motion, nsMsgKey* pResultKey, + nsMsgViewIndex* pResultIndex, + nsMsgViewIndex* pThreadIndex, bool wrap) { + NS_ENSURE_ARG_POINTER(pResultKey); + NS_ENSURE_ARG_POINTER(pResultIndex); + NS_ENSURE_ARG_POINTER(pThreadIndex); + + int32_t currentIndex; + nsMsgViewIndex startIndex; + + if (!mTreeSelection) { + // We must be in stand alone message mode. + currentIndex = FindViewIndex(m_currentlyDisplayedMsgKey); + } else { + nsresult rv = mTreeSelection->GetCurrentIndex(¤tIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + + startIndex = currentIndex; + return nsMsgDBView::NavigateFromPos(motion, startIndex, pResultKey, + pResultIndex, pThreadIndex, wrap); +} + +nsresult nsMsgDBView::NavigateFromPos(nsMsgNavigationTypeValue motion, + nsMsgViewIndex startIndex, + nsMsgKey* pResultKey, + nsMsgViewIndex* pResultIndex, + nsMsgViewIndex* pThreadIndex, bool wrap) { + nsresult rv = NS_OK; + nsMsgKey resultThreadKey; + nsMsgViewIndex curIndex; + nsMsgViewIndex lastIndex = + (GetSize() > 0) ? (nsMsgViewIndex)GetSize() - 1 : nsMsgViewIndex_None; + nsMsgViewIndex threadIndex = nsMsgViewIndex_None; + + // If there aren't any messages in the view, bail out. + if (GetSize() <= 0) { + *pResultIndex = nsMsgViewIndex_None; + *pResultKey = nsMsgKey_None; + return NS_OK; + } + *pResultKey = nsMsgKey_None; + + switch (motion) { + case nsMsgNavigationType::firstMessage: + *pResultIndex = 0; + *pResultKey = m_keys[0]; + break; + case nsMsgNavigationType::nextMessage: + // Return same index and id on next on last message. + *pResultIndex = std::min(startIndex + 1, lastIndex); + *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::previousMessage: + if (startIndex != nsMsgViewIndex_None && startIndex > 0) { + *pResultIndex = startIndex - 1; + } + if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::lastMessage: + *pResultIndex = lastIndex; + *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::firstFlagged: + rv = FindFirstFlagged(pResultIndex); + if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::nextFlagged: + rv = FindNextFlagged(startIndex + 1, pResultIndex); + if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::previousFlagged: + rv = FindPrevFlagged(startIndex, pResultIndex); + if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::firstNew: + rv = FindFirstNew(pResultIndex); + if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::firstUnreadMessage: + startIndex = nsMsgViewIndex_None; + // Note fall through - is this motion ever used? + [[fallthrough]]; + case nsMsgNavigationType::nextUnreadMessage: + for (curIndex = (startIndex == nsMsgViewIndex_None) ? 0 : startIndex; + curIndex <= lastIndex && lastIndex != nsMsgViewIndex_None; + curIndex++) { + uint32_t flags = m_flags[curIndex]; + // Don't return start index since navigate should move. + if (!(flags & (nsMsgMessageFlags::Read | MSG_VIEW_FLAG_DUMMY)) && + (curIndex != startIndex)) { + *pResultIndex = curIndex; + *pResultKey = m_keys[*pResultIndex]; + break; + } + + // Check for collapsed thread with new children. + if ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) && + flags & MSG_VIEW_FLAG_ISTHREAD && + flags & nsMsgMessageFlags::Elided) { + nsCOMPtr<nsIMsgThread> threadHdr; + GetThreadContainingIndex(curIndex, getter_AddRefs(threadHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(threadHdr, "threadHdr is null"); + if (!threadHdr) continue; + + uint32_t numUnreadChildren; + threadHdr->GetNumUnreadChildren(&numUnreadChildren); + if (numUnreadChildren > 0) { + uint32_t numExpanded; + ExpandByIndex(curIndex, &numExpanded); + lastIndex += numExpanded; + if (pThreadIndex) *pThreadIndex = curIndex; + } + } + } + + if (curIndex > lastIndex) { + // Wrap around by starting at index 0. + if (wrap) { + nsMsgKey startKey = GetAt(startIndex); + rv = NavigateFromPos(nsMsgNavigationType::nextUnreadMessage, + nsMsgViewIndex_None, pResultKey, pResultIndex, + pThreadIndex, false); + + if (*pResultKey == startKey) { + // wrapped around and found start message! + *pResultIndex = nsMsgViewIndex_None; + *pResultKey = nsMsgKey_None; + } + } else { + *pResultIndex = nsMsgViewIndex_None; + *pResultKey = nsMsgKey_None; + } + } + break; + case nsMsgNavigationType::previousUnreadMessage: + if (!IsValidIndex(startIndex)) break; + + rv = FindPrevUnread(m_keys[startIndex], pResultKey, &resultThreadKey); + if (NS_SUCCEEDED(rv)) { + *pResultIndex = FindViewIndex(*pResultKey); + if (*pResultKey != resultThreadKey && + (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { + threadIndex = GetThreadIndex(*pResultIndex); + if (*pResultIndex == nsMsgViewIndex_None) { + nsCOMPtr<nsIMsgThread> threadHdr; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = m_db->GetMsgHdrForKey(*pResultKey, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(threadHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(threadHdr, "threadHdr is null"); + if (threadHdr) break; + + uint32_t numUnreadChildren; + threadHdr->GetNumUnreadChildren(&numUnreadChildren); + if (numUnreadChildren > 0) { + uint32_t numExpanded; + ExpandByIndex(threadIndex, &numExpanded); + } + + *pResultIndex = FindViewIndex(*pResultKey); + } + } + + if (pThreadIndex) *pThreadIndex = threadIndex; + } + break; + case nsMsgNavigationType::lastUnreadMessage: + break; + case nsMsgNavigationType::nextUnreadThread: + if (startIndex != nsMsgViewIndex_None) { + ApplyCommandToIndices(nsMsgViewCommandType::markThreadRead, + {startIndex}); + } + + return NavigateFromPos(nsMsgNavigationType::nextUnreadMessage, startIndex, + pResultKey, pResultIndex, pThreadIndex, true); + case nsMsgNavigationType::toggleThreadKilled: { + bool resultKilled; + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + ToggleIgnored(selection, &threadIndex, &resultKilled); + if (resultKilled) { + return NavigateFromPos(nsMsgNavigationType::nextUnreadThread, + threadIndex, pResultKey, pResultIndex, + pThreadIndex, true); + } else { + *pResultIndex = nsMsgViewIndex_None; + *pResultKey = nsMsgKey_None; + return NS_OK; + } + } + case nsMsgNavigationType::toggleSubthreadKilled: { + bool resultKilled; + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + ToggleMessageKilled(selection, &threadIndex, &resultKilled); + if (resultKilled) { + return NavigateFromPos(nsMsgNavigationType::nextUnreadMessage, + threadIndex, pResultKey, pResultIndex, + pThreadIndex, true); + } else { + *pResultIndex = nsMsgViewIndex_None; + *pResultKey = nsMsgKey_None; + return NS_OK; + } + } + // Check where navigate says this will take us. If we have the message + // in the view, return it. Otherwise, return an error. + case nsMsgNavigationType::back: + case nsMsgNavigationType::forward: + // Handled purely in JS. + *pResultIndex = nsMsgViewIndex_None; + *pResultKey = nsMsgKey_None; + break; + default: + NS_ERROR("unsupported motion"); + break; + } + + return NS_OK; +} + +// Note that these routines do NOT expand collapsed threads! This mimics the +// old behaviour, but it's also because we don't remember whether a thread +// contains a flagged message the same way we remember if a thread contains +// new messages. It would be painful to dive down into each collapsed thread +// to update navigate status. We could cache this info, but it would still be +// expensive the first time this status needs to get updated. +nsresult nsMsgDBView::FindNextFlagged(nsMsgViewIndex startIndex, + nsMsgViewIndex* pResultIndex) { + nsMsgViewIndex lastIndex = (nsMsgViewIndex)GetSize() - 1; + nsMsgViewIndex curIndex; + + *pResultIndex = nsMsgViewIndex_None; + + if (GetSize() > 0) { + for (curIndex = startIndex; curIndex <= lastIndex; curIndex++) { + uint32_t flags = m_flags[curIndex]; + if (flags & nsMsgMessageFlags::Marked) { + *pResultIndex = curIndex; + break; + } + } + } + + return NS_OK; +} + +nsresult nsMsgDBView::FindFirstNew(nsMsgViewIndex* pResultIndex) { + if (m_db) { + nsMsgKey firstNewKey = nsMsgKey_None; + m_db->GetFirstNew(&firstNewKey); + *pResultIndex = (firstNewKey != nsMsgKey_None) ? FindKey(firstNewKey, true) + : nsMsgViewIndex_None; + } + + return NS_OK; +} + +nsresult nsMsgDBView::FindPrevUnread(nsMsgKey startKey, nsMsgKey* pResultKey, + nsMsgKey* resultThreadId) { + nsMsgViewIndex startIndex = FindViewIndex(startKey); + nsMsgViewIndex curIndex = startIndex; + nsresult rv = NS_MSG_MESSAGE_NOT_FOUND; + + if (startIndex == nsMsgViewIndex_None) return NS_MSG_MESSAGE_NOT_FOUND; + + *pResultKey = nsMsgKey_None; + if (resultThreadId) *resultThreadId = nsMsgKey_None; + + for (; (int)curIndex >= 0 && (*pResultKey == nsMsgKey_None); curIndex--) { + uint32_t flags = m_flags[curIndex]; + if (curIndex != startIndex && flags & MSG_VIEW_FLAG_ISTHREAD && + flags & nsMsgMessageFlags::Elided) { + NS_ERROR("fix this"); + // nsMsgKey threadId = m_keys[curIndex]; + // rv = m_db->GetUnreadKeyInThread(threadId, pResultKey, resultThreadId); + if (NS_SUCCEEDED(rv) && (*pResultKey != nsMsgKey_None)) break; + } + + if (!(flags & (nsMsgMessageFlags::Read | MSG_VIEW_FLAG_DUMMY)) && + (curIndex != startIndex)) { + *pResultKey = m_keys[curIndex]; + rv = NS_OK; + break; + } + } + + // Found unread message but we don't know the thread. + NS_ASSERTION(!(*pResultKey != nsMsgKey_None && resultThreadId && + *resultThreadId == nsMsgKey_None), + "fix this"); + return rv; +} + +nsresult nsMsgDBView::FindFirstFlagged(nsMsgViewIndex* pResultIndex) { + return FindNextFlagged(0, pResultIndex); +} + +nsresult nsMsgDBView::FindPrevFlagged(nsMsgViewIndex startIndex, + nsMsgViewIndex* pResultIndex) { + nsMsgViewIndex curIndex; + + *pResultIndex = nsMsgViewIndex_None; + + if (GetSize() > 0 && IsValidIndex(startIndex)) { + curIndex = startIndex; + do { + if (curIndex != 0) curIndex--; + + uint32_t flags = m_flags[curIndex]; + if (flags & nsMsgMessageFlags::Marked) { + *pResultIndex = curIndex; + break; + } + } while (curIndex != 0); + } + + return NS_OK; +} + +bool nsMsgDBView::IsValidIndex(nsMsgViewIndex index) { + return index != nsMsgViewIndex_None && + (index < (nsMsgViewIndex)m_keys.Length()); +} + +nsresult nsMsgDBView::OrExtraFlag(nsMsgViewIndex index, uint32_t orflag) { + uint32_t flag; + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + flag = m_flags[index]; + flag |= orflag; + m_flags[index] = flag; + OnExtraFlagChanged(index, flag); + return NS_OK; +} + +nsresult nsMsgDBView::AndExtraFlag(nsMsgViewIndex index, uint32_t andflag) { + uint32_t flag; + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + flag = m_flags[index]; + flag &= andflag; + m_flags[index] = flag; + OnExtraFlagChanged(index, flag); + return NS_OK; +} + +nsresult nsMsgDBView::SetExtraFlag(nsMsgViewIndex index, uint32_t extraflag) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + m_flags[index] = extraflag; + OnExtraFlagChanged(index, extraflag); + return NS_OK; +} + +nsresult nsMsgDBView::ToggleIgnored(nsTArray<nsMsgViewIndex> const& selection, + nsMsgViewIndex* resultIndex, + bool* resultToggleState) { + nsCOMPtr<nsIMsgThread> thread; + + // Ignored state is toggled based on the first selected thread. + nsMsgViewIndex threadIndex = + GetThreadFromMsgIndex(selection[0], getter_AddRefs(thread)); + NS_ENSURE_STATE(thread); + uint32_t threadFlags; + thread->GetFlags(&threadFlags); + uint32_t ignored = threadFlags & nsMsgMessageFlags::Ignored; + + // Process threads in reverse order. + // Otherwise collapsing the threads will invalidate the indices. + threadIndex = nsMsgViewIndex_None; + uint32_t numIndices = selection.Length(); + while (numIndices) { + numIndices--; + if (selection[numIndices] < threadIndex) { + threadIndex = + GetThreadFromMsgIndex(selection[numIndices], getter_AddRefs(thread)); + thread->GetFlags(&threadFlags); + if ((threadFlags & nsMsgMessageFlags::Ignored) == ignored) + SetThreadIgnored(thread, threadIndex, !ignored); + } + } + + if (resultIndex) *resultIndex = threadIndex; + + if (resultToggleState) *resultToggleState = !ignored; + + return NS_OK; +} + +nsresult nsMsgDBView::ToggleMessageKilled( + nsTArray<nsMsgViewIndex> const& selection, nsMsgViewIndex* resultIndex, + bool* resultToggleState) { + NS_ENSURE_ARG_POINTER(resultToggleState); + + nsCOMPtr<nsIMsgDBHdr> header; + // Ignored state is toggled based on the first selected message. + nsresult rv = GetMsgHdrForViewIndex(selection[0], getter_AddRefs(header)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t msgFlags; + header->GetFlags(&msgFlags); + uint32_t ignored = msgFlags & nsMsgMessageFlags::Ignored; + + // Process messages in reverse order. + // Otherwise the indices may be invalidated. + nsMsgViewIndex msgIndex = nsMsgViewIndex_None; + uint32_t numIndices = selection.Length(); + while (numIndices) { + numIndices--; + if (selection[numIndices] < msgIndex) { + msgIndex = selection[numIndices]; + rv = GetMsgHdrForViewIndex(msgIndex, getter_AddRefs(header)); + NS_ENSURE_SUCCESS(rv, rv); + header->GetFlags(&msgFlags); + if ((msgFlags & nsMsgMessageFlags::Ignored) == ignored) + SetSubthreadKilled(header, msgIndex, !ignored); + } + } + + if (resultIndex) *resultIndex = msgIndex; + + if (resultToggleState) *resultToggleState = !ignored; + + return NS_OK; +} + +nsMsgViewIndex nsMsgDBView::GetThreadFromMsgIndex(nsMsgViewIndex index, + nsIMsgThread** threadHdr) { + if (threadHdr == nullptr) return nsMsgViewIndex_None; + nsMsgKey msgKey = GetAt(index); + + nsresult rv = GetThreadContainingIndex(index, threadHdr); + NS_ENSURE_SUCCESS(rv, nsMsgViewIndex_None); + + if (*threadHdr == nullptr) return nsMsgViewIndex_None; + + nsMsgKey threadKey; + (*threadHdr)->GetThreadKey(&threadKey); + nsMsgViewIndex threadIndex; + if (msgKey != threadKey) + threadIndex = GetIndexOfFirstDisplayedKeyInThread(*threadHdr); + else + threadIndex = index; + return threadIndex; +} + +nsresult nsMsgDBView::ToggleWatched(nsTArray<nsMsgViewIndex> const& selection) { + MOZ_ASSERT(!selection.IsEmpty()); + nsCOMPtr<nsIMsgThread> thread; + + // Watched state is toggled based on the first selected thread. + nsMsgViewIndex threadIndex = + GetThreadFromMsgIndex(selection[0], getter_AddRefs(thread)); + NS_ENSURE_STATE(thread); + uint32_t threadFlags; + thread->GetFlags(&threadFlags); + uint32_t watched = threadFlags & nsMsgMessageFlags::Watched; + + // Process threads in reverse order for consistency with ToggleIgnored. + threadIndex = nsMsgViewIndex_None; + uint32_t numIndices = selection.Length(); + while (numIndices) { + numIndices--; + if (selection[numIndices] < threadIndex) { + threadIndex = + GetThreadFromMsgIndex(selection[numIndices], getter_AddRefs(thread)); + thread->GetFlags(&threadFlags); + if ((threadFlags & nsMsgMessageFlags::Watched) == watched) + SetThreadWatched(thread, threadIndex, !watched); + } + } + + return NS_OK; +} + +nsresult nsMsgDBView::SetThreadIgnored(nsIMsgThread* thread, + nsMsgViewIndex threadIndex, + bool ignored) { + if (!IsValidIndex(threadIndex)) return NS_MSG_INVALID_DBVIEW_INDEX; + + NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed); + if (ignored) { + nsTArray<nsMsgKey> idsMarkedRead; + MarkThreadRead(thread, threadIndex, idsMarkedRead, true); + CollapseByIndex(threadIndex, nullptr); + } + + if (!m_db) return NS_ERROR_FAILURE; + + return m_db->MarkThreadIgnored(thread, m_keys[threadIndex], ignored, this); +} + +nsresult nsMsgDBView::SetSubthreadKilled(nsIMsgDBHdr* header, + nsMsgViewIndex msgIndex, + bool ignored) { + if (!IsValidIndex(msgIndex)) return NS_MSG_INVALID_DBVIEW_INDEX; + + NoteChange(msgIndex, 1, nsMsgViewNotificationCode::changed); + + if (!m_db) return NS_ERROR_FAILURE; + + nsresult rv = m_db->MarkHeaderKilled(header, ignored, this); + NS_ENSURE_SUCCESS(rv, rv); + + if (ignored) { + nsCOMPtr<nsIMsgThread> thread; + nsresult rv; + rv = GetThreadContainingMsgHdr(header, getter_AddRefs(thread)); + // So we didn't mark threads read. + if (NS_FAILED(rv)) return NS_OK; + + uint32_t children, current; + thread->GetNumChildren(&children); + + nsMsgKey headKey; + header->GetMessageKey(&headKey); + + for (current = 0; current < children; current++) { + nsMsgKey newKey; + thread->GetChildKeyAt(current, &newKey); + if (newKey == headKey) break; + } + + // Process all messages, starting with this message. + for (; current < children; current++) { + nsCOMPtr<nsIMsgDBHdr> nextHdr; + bool isKilled; + + thread->GetChildHdrAt(current, getter_AddRefs(nextHdr)); + nextHdr->GetIsKilled(&isKilled); + + // Ideally, the messages should stop processing here. + // However, the children are ordered not by thread... + if (isKilled) nextHdr->MarkRead(true); + } + } + + return NS_OK; +} + +nsresult nsMsgDBView::SetThreadWatched(nsIMsgThread* thread, + nsMsgViewIndex index, bool watched) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + nsresult rv = m_db->MarkThreadWatched(thread, m_keys[index], watched, this); + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::GetMsgFolder(nsIMsgFolder** aMsgFolder) { + NS_ENSURE_ARG_POINTER(aMsgFolder); + NS_IF_ADDREF(*aMsgFolder = m_folder); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetViewFolder(nsIMsgFolder* aMsgFolder) { + m_viewFolder = aMsgFolder; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetViewFolder(nsIMsgFolder** aMsgFolder) { + NS_ENSURE_ARG_POINTER(aMsgFolder); + NS_IF_ADDREF(*aMsgFolder = m_viewFolder); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetNumSelected(uint32_t* aNumSelected) { + NS_ENSURE_ARG_POINTER(aNumSelected); + + if (!mTreeSelection) { + // No tree selection can mean we're in the stand alone mode. + *aNumSelected = (m_currentlyDisplayedMsgKey != nsMsgKey_None) ? 1 : 0; + return NS_OK; + } + + bool includeCollapsedMsgs = OperateOnMsgsInCollapsedThreads(); + + // We call this a lot from the front end JS, so make it fast. + nsresult rv = mTreeSelection->GetCount((int32_t*)aNumSelected); + if (!*aNumSelected || !includeCollapsedMsgs || + !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) + return rv; + + int32_t numSelectedIncludingCollapsed = *aNumSelected; + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + int32_t numIndices = selection.Length(); + // Iterate over the selection, counting up the messages in collapsed + // threads. + for (int32_t i = 0; i < numIndices; i++) { + if (m_flags[selection[i]] & nsMsgMessageFlags::Elided) { + int32_t collapsedCount; + ExpansionDelta(selection[i], &collapsedCount); + numSelectedIncludingCollapsed += collapsedCount; + } + } + + *aNumSelected = numSelectedIncludingCollapsed; + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::GetNumMsgsInView(int32_t* aNumMsgs) { + NS_ENSURE_ARG_POINTER(aNumMsgs); + return (m_folder) ? m_folder->GetTotalMessages(false, aNumMsgs) + : NS_ERROR_FAILURE; +} + +/** + * @note For the IMAP delete model, this applies to both deleting and + * undeleting a message. + */ +NS_IMETHODIMP +nsMsgDBView::GetMsgToSelectAfterDelete(nsMsgViewIndex* msgToSelectAfterDelete) { + NS_ENSURE_ARG_POINTER(msgToSelectAfterDelete); + *msgToSelectAfterDelete = nsMsgViewIndex_None; + + bool isMultiSelect = false; + int32_t startFirstRange = nsMsgViewIndex_None; + int32_t endFirstRange = nsMsgViewIndex_None; + if (!mTreeSelection) { + // If we don't have a tree selection then we must be in stand alone mode. + // return the index of the current message key as the first selected index. + *msgToSelectAfterDelete = FindViewIndex(m_currentlyDisplayedMsgKey); + } else { + int32_t selectionCount; + int32_t startRange; + int32_t endRange; + nsresult rv = mTreeSelection->GetRangeCount(&selectionCount); + NS_ENSURE_SUCCESS(rv, rv); + for (int32_t i = 0; i < selectionCount; i++) { + rv = mTreeSelection->GetRangeAt(i, &startRange, &endRange); + NS_ENSURE_SUCCESS(rv, rv); + + // Save off the first range in case we need it later. + if (i == 0) { + startFirstRange = startRange; + endFirstRange = endRange; + } else { + // If the tree selection is goofy (eg adjacent or overlapping ranges), + // complain about it, but don't try and cope. Just live with the fact + // that one of the deleted messages is going to end up selected. + NS_WARNING_ASSERTION( + endFirstRange != startRange, + "goofy tree selection state: two ranges are adjacent!"); + } + + *msgToSelectAfterDelete = + std::min(*msgToSelectAfterDelete, (nsMsgViewIndex)startRange); + } + + // Multiple selection either using Ctrl, Shift, or one of the affordances + // to select an entire thread. + isMultiSelect = (selectionCount > 1 || (endRange - startRange) > 0); + } + + if (*msgToSelectAfterDelete == nsMsgViewIndex_None) return NS_OK; + + nsCOMPtr<nsIMsgFolder> folder; + GetMsgFolder(getter_AddRefs(folder)); + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder); + bool thisIsImapFolder = (imapFolder != nullptr); + // Need to update the imap-delete model, can change more than once in a + // session. + if (thisIsImapFolder) GetImapDeleteModel(nullptr); + + // If mail.delete_matches_sort_order is true, + // for views sorted in descending order (newest at the top), make + // msgToSelectAfterDelete advance in the same direction as the sort order. + bool deleteMatchesSort = false; + if (m_sortOrder == nsMsgViewSortOrder::descending && + *msgToSelectAfterDelete) { + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + prefBranch->GetBoolPref("mail.delete_matches_sort_order", + &deleteMatchesSort); + } + + if (mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) { + if (isMultiSelect) { + if (deleteMatchesSort) + *msgToSelectAfterDelete = startFirstRange - 1; + else + *msgToSelectAfterDelete = endFirstRange + 1; + } else { + if (deleteMatchesSort) + *msgToSelectAfterDelete -= 1; + else + *msgToSelectAfterDelete += 1; + } + } else if (deleteMatchesSort) { + *msgToSelectAfterDelete -= 1; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetRemoveRowOnMoveOrDelete(bool* aRemoveRowOnMoveOrDelete) { + NS_ENSURE_ARG_POINTER(aRemoveRowOnMoveOrDelete); + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_folder); + if (!imapFolder) { + *aRemoveRowOnMoveOrDelete = true; + return NS_OK; + } + + // Need to update the imap-delete model, can change more than once in a + // session. + GetImapDeleteModel(nullptr); + + // Unlike the other imap delete models, "mark as deleted" does not remove + // rows on delete (or move). + *aRemoveRowOnMoveOrDelete = + (mDeleteModel != nsMsgImapDeleteModels::IMAPDelete); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetCurrentlyDisplayedMessage( + nsMsgViewIndex* currentlyDisplayedMessage) { + NS_ENSURE_ARG_POINTER(currentlyDisplayedMessage); + *currentlyDisplayedMessage = FindViewIndex(m_currentlyDisplayedMsgKey); + return NS_OK; +} + +// If nothing selected, return an NS_ERROR. +NS_IMETHODIMP +nsMsgDBView::GetHdrForFirstSelectedMessage(nsIMsgDBHdr** hdr) { + NS_ENSURE_ARG_POINTER(hdr); + + nsresult rv; + nsMsgKey key; + rv = GetKeyForFirstSelectedMessage(&key); + // Don't assert, it is legal for nothing to be selected. + if (NS_FAILED(rv)) return rv; + + if (key == nsMsgKey_None) { + *hdr = nullptr; + return NS_OK; + } + + if (!m_db) return NS_MSG_MESSAGE_NOT_FOUND; + + rv = m_db->GetMsgHdrForKey(key, hdr); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +// If nothing selected, return an NS_ERROR. +NS_IMETHODIMP +nsMsgDBView::GetURIForFirstSelectedMessage(nsACString& uri) { + nsresult rv; + nsMsgViewIndex viewIndex; + rv = GetViewIndexForFirstSelectedMsg(&viewIndex); + // Don't assert, it is legal for nothing to be selected. + if (NS_FAILED(rv)) return rv; + + return GetURIForViewIndex(viewIndex, uri); +} + +NS_IMETHODIMP +nsMsgDBView::OnDeleteCompleted(bool aSucceeded) { + if (m_deletingRows && aSucceeded) { + uint32_t numIndices = mIndicesToNoteChange.Length(); + if (numIndices && (mTree || mJSTree)) { + if (numIndices > 1) mIndicesToNoteChange.Sort(); + + // The call to NoteChange() has to happen after we are done removing the + // keys as NoteChange() will call RowCountChanged() which will call our + // GetRowCount(). + if (numIndices > 1) { + if (mTree) mTree->BeginUpdateBatch(); + if (mJSTree) mJSTree->BeginUpdateBatch(); + } + + for (uint32_t i = 0; i < numIndices; i++) + NoteChange(mIndicesToNoteChange[i], -1, + nsMsgViewNotificationCode::insertOrDelete); + + if (numIndices > 1) { + if (mTree) mTree->EndUpdateBatch(); + if (mJSTree) mJSTree->EndUpdateBatch(); + } + } + + mIndicesToNoteChange.Clear(); + } + + m_deletingRows = false; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetDb(nsIMsgDatabase** aDB) { + NS_ENSURE_ARG_POINTER(aDB); + NS_IF_ADDREF(*aDB = m_db); + return NS_OK; +} + +bool nsMsgDBView::OfflineMsgSelected( + nsTArray<nsMsgViewIndex> const& selection) { + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder); + if (localFolder) { + return true; + } + + for (nsMsgViewIndex viewIndex : selection) { + // For cross-folder saved searches, we need to check if any message + // is in a local folder. + if (!m_folder) { + nsCOMPtr<nsIMsgFolder> folder; + GetFolderForViewIndex(viewIndex, getter_AddRefs(folder)); + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(folder); + if (localFolder) { + return true; + } + } + + uint32_t flags = m_flags[viewIndex]; + if ((flags & nsMsgMessageFlags::Offline)) { + return true; + } + } + + return false; +} + +bool nsMsgDBView::NonDummyMsgSelected( + nsTArray<nsMsgViewIndex> const& selection) { + bool includeCollapsedMsgs = OperateOnMsgsInCollapsedThreads(); + + for (nsMsgViewIndex viewIndex : selection) { + uint32_t flags = m_flags[viewIndex]; + // We now treat having a collapsed dummy message selected as if + // the whole group was selected so we can apply commands to the group. + if (!(flags & MSG_VIEW_FLAG_DUMMY) || + (flags & nsMsgMessageFlags::Elided && includeCollapsedMsgs)) { + return true; + } + } + + return false; +} + +NS_IMETHODIMP +nsMsgDBView::GetViewIndexForFirstSelectedMsg(nsMsgViewIndex* aViewIndex) { + NS_ENSURE_ARG_POINTER(aViewIndex); + // If we don't have a tree selection we must be in stand alone mode... + if (!mTreeSelection) { + *aViewIndex = m_currentlyDisplayedViewIndex; + return NS_OK; + } + + int32_t startRange; + int32_t endRange; + nsresult rv = mTreeSelection->GetRangeAt(0, &startRange, &endRange); + // Don't assert, it is legal for nothing to be selected. + if (NS_FAILED(rv)) return rv; + + // Check that the first index is valid, it may not be if nothing is selected. + if (startRange < 0 || uint32_t(startRange) >= GetSize()) + return NS_ERROR_UNEXPECTED; + + *aViewIndex = startRange; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetKeyForFirstSelectedMessage(nsMsgKey* key) { + NS_ENSURE_ARG_POINTER(key); + // If we don't have a tree selection we must be in stand alone mode... + if (!mTreeSelection) { + *key = m_currentlyDisplayedMsgKey; + return NS_OK; + } + + int32_t selectionCount; + mTreeSelection->GetRangeCount(&selectionCount); + if (selectionCount == 0) { + *key = nsMsgKey_None; + return NS_OK; + } + + int32_t startRange; + int32_t endRange; + nsresult rv = mTreeSelection->GetRangeAt(0, &startRange, &endRange); + // Don't assert, it is legal for nothing to be selected. + if (NS_FAILED(rv)) return rv; + + // Check that the first index is valid, it may not be if nothing is selected. + if (startRange < 0 || uint32_t(startRange) >= GetSize()) + return NS_ERROR_UNEXPECTED; + + if (m_flags[startRange] & MSG_VIEW_FLAG_DUMMY) + return NS_MSG_INVALID_DBVIEW_INDEX; + + *key = m_keys[startRange]; + return NS_OK; +} + +nsCOMArray<nsIMsgFolder>* nsMsgDBView::GetFolders() { return nullptr; } + +nsresult nsMsgDBView::AdjustRowCount(int32_t rowCountBeforeSort, + int32_t rowCountAfterSort) { + int32_t rowChange = rowCountAfterSort - rowCountBeforeSort; + + if (rowChange) { + // This is not safe to use when you have a selection. + // RowCountChanged() will call AdjustSelection(). + uint32_t numSelected = 0; + GetNumSelected(&numSelected); + NS_ASSERTION( + numSelected == 0, + "it is not save to call AdjustRowCount() when you have a selection"); + + if (mTree) mTree->RowCountChanged(0, rowChange); + if (mJSTree) mJSTree->RowCountChanged(0, rowChange); + } + + return NS_OK; +} + +nsresult nsMsgDBView::GetImapDeleteModel(nsIMsgFolder* folder) { + nsresult rv = NS_OK; + nsCOMPtr<nsIMsgIncomingServer> server; + // For the search view. + if (folder) + folder->GetServer(getter_AddRefs(server)); + else if (m_folder) + m_folder->GetServer(getter_AddRefs(server)); + + nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server, &rv); + if (NS_SUCCEEDED(rv) && imapServer) imapServer->GetDeleteModel(&mDeleteModel); + + return rv; +} + +// +// CanDrop +// +// Can't drop on the thread pane. +// +NS_IMETHODIMP +nsMsgDBView::CanDrop(int32_t index, int32_t orient, + mozilla::dom::DataTransfer* dataTransfer, bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = false; + + return NS_OK; +} + +// +// Drop +// +// Can't drop on the thread pane. +// +NS_IMETHODIMP +nsMsgDBView::Drop(int32_t row, int32_t orient, + mozilla::dom::DataTransfer* dataTransfer) { + return NS_OK; +} + +// +// IsSorted +// +// ... +// +NS_IMETHODIMP +nsMsgDBView::IsSorted(bool* _retval) { + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SelectFolderMsgByKey(nsIMsgFolder* aFolder, nsMsgKey aKey) { + NS_ENSURE_ARG_POINTER(aFolder); + if (aKey == nsMsgKey_None) return NS_ERROR_FAILURE; + + // This is OK for non search views. + + nsMsgViewIndex viewIndex = FindKey(aKey, true /* expand */); + + if (mTree) mTreeSelection->SetCurrentIndex(viewIndex); + + // Make sure the current message is once again visible in the thread pane + // so we don't have to go search for it in the thread pane. + if (mTree && viewIndex != nsMsgViewIndex_None) { + mTreeSelection->Select(viewIndex); + mTree->EnsureRowIsVisible(viewIndex); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SelectMsgByKey(nsMsgKey aKey) { + NS_ASSERTION(aKey != nsMsgKey_None, "bad key"); + if (aKey == nsMsgKey_None) return NS_OK; + + // Use SaveAndClearSelection() + // and RestoreSelection() so that we'll clear the current selection + // but pass in a different key array so that we'll + // select (and load) the desired message. + + AutoTArray<nsMsgKey, 1> preservedSelection; + nsresult rv = SaveAndClearSelection(nullptr, preservedSelection); + NS_ENSURE_SUCCESS(rv, rv); + + // Now, restore our desired selection. + AutoTArray<nsMsgKey, 1> keyArray; + keyArray.AppendElement(aKey); + + // If the key was not found + // (this can happen with "remember last selected message") + // nothing will be selected. + rv = RestoreSelection(aKey, keyArray); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::CloneDBView(nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater, + nsIMsgDBView** _retval) { + nsMsgDBView* newMsgDBView = new nsMsgDBView(); + + nsresult rv = + CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater); + NS_ENSURE_SUCCESS(rv, rv); + + NS_IF_ADDREF(*_retval = newMsgDBView); + return NS_OK; +} + +nsresult nsMsgDBView::CopyDBView(nsMsgDBView* aNewMsgDBView, + nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater) { + NS_ENSURE_ARG_POINTER(aNewMsgDBView); + if (aMsgWindow) { + aNewMsgDBView->mMsgWindowWeak = do_GetWeakReference(aMsgWindow); + aMsgWindow->SetOpenFolder(m_viewFolder ? m_viewFolder : m_folder); + } + + aNewMsgDBView->mMessengerWeak = do_GetWeakReference(aMessengerInstance); + aNewMsgDBView->mCommandUpdater = do_GetWeakReference(aCmdUpdater); + aNewMsgDBView->m_folder = m_folder; + aNewMsgDBView->m_viewFlags = m_viewFlags; + aNewMsgDBView->m_sortOrder = m_sortOrder; + aNewMsgDBView->m_sortType = m_sortType; + aNewMsgDBView->m_curCustomColumn = m_curCustomColumn; + aNewMsgDBView->m_secondarySort = m_secondarySort; + aNewMsgDBView->m_secondarySortOrder = m_secondarySortOrder; + aNewMsgDBView->m_secondaryCustomColumn = m_secondaryCustomColumn; + aNewMsgDBView->m_db = m_db; + if (m_db) aNewMsgDBView->m_db->AddListener(aNewMsgDBView); + + aNewMsgDBView->mIsNews = mIsNews; + aNewMsgDBView->mIsRss = mIsRss; + aNewMsgDBView->mIsXFVirtual = mIsXFVirtual; + aNewMsgDBView->mShowSizeInLines = mShowSizeInLines; + aNewMsgDBView->mDeleteModel = mDeleteModel; + aNewMsgDBView->m_flags = m_flags.Clone(); + aNewMsgDBView->m_levels = m_levels.Clone(); + aNewMsgDBView->m_keys = m_keys.Clone(); + + aNewMsgDBView->m_customColumnHandlerIDs = m_customColumnHandlerIDs.Clone(); + aNewMsgDBView->m_customColumnHandlers.AppendObjects(m_customColumnHandlers); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSearchSession(nsIMsgSearchSession** aSession) { + NS_ASSERTION(false, "should be overridden by child class"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDBView::SetSearchSession(nsIMsgSearchSession* aSession) { + NS_ASSERTION(false, "should be overridden by child class"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDBView::GetSupportsThreading(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = true; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::FindIndexFromKey(nsMsgKey aMsgKey, bool aExpand, + nsMsgViewIndex* aIndex) { + NS_ENSURE_ARG_POINTER(aIndex); + *aIndex = FindKey(aMsgKey, aExpand); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::FindIndexOfMsgHdr(nsIMsgDBHdr* aMsgHdr, bool aExpand, + nsMsgViewIndex* aIndex) { + NS_ENSURE_ARG(aMsgHdr); + NS_ENSURE_ARG_POINTER(aIndex); + + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + nsMsgViewIndex threadIndex = ThreadIndexOfMsgHdr(aMsgHdr); + if (threadIndex != nsMsgViewIndex_None) { + if (aExpand && (m_flags[threadIndex] & nsMsgMessageFlags::Elided)) + ExpandByIndex(threadIndex, nullptr); + + *aIndex = FindHdr(aMsgHdr, threadIndex); + } else { + *aIndex = nsMsgViewIndex_None; + } + } else { + *aIndex = FindHdr(aMsgHdr); + } + + return NS_OK; +} + +static void getDateFormatPref(nsIPrefBranch* _prefBranch, + const char* _prefLocalName, + nsDateFormatSelectorComm& _format) { + // Read. + int32_t nFormatSetting(0); + nsresult result = _prefBranch->GetIntPref(_prefLocalName, &nFormatSetting); + if (NS_SUCCEEDED(result)) { + // Translate. + nsDateFormatSelectorComm res; + res = static_cast<nsDateFormatSelectorComm>(nFormatSetting); + // Transfer if valid. + if (res >= kDateFormatNone && res <= kDateFormatShort) + _format = res; + else if (res == kDateFormatWeekday) + _format = res; + } +} + +nsresult nsMsgDBView::InitDisplayFormats() { + m_dateFormatsInitialized = true; + + nsresult rv = NS_OK; + nsCOMPtr<nsIPrefService> prefs = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIPrefBranch> dateFormatPrefs; + rv = prefs->GetBranch("mail.ui.display.dateformat.", + getter_AddRefs(dateFormatPrefs)); + NS_ENSURE_SUCCESS(rv, rv); + + getDateFormatPref(dateFormatPrefs, "default", m_dateFormatDefault); + getDateFormatPref(dateFormatPrefs, "thisweek", m_dateFormatThisWeek); + getDateFormatPref(dateFormatPrefs, "today", m_dateFormatToday); + return rv; +} + +void nsMsgDBView::SetMRUTimeForFolder(nsIMsgFolder* folder) { + uint32_t seconds; + PRTime2Seconds(PR_Now(), &seconds); + nsAutoCString nowStr; + nowStr.AppendInt(seconds); + folder->SetStringProperty(MRU_TIME_PROPERTY, nowStr); +} + +nsMsgDBView::nsMsgViewHdrEnumerator::nsMsgViewHdrEnumerator(nsMsgDBView* view) { + // We need to clone the view because the caller may clear the + // current view immediately. It also makes it easier to expand all + // if we're working on a copy. + nsCOMPtr<nsIMsgDBView> clonedView; + view->CloneDBView(nullptr, nullptr, nullptr, getter_AddRefs(clonedView)); + m_view = static_cast<nsMsgDBView*>(clonedView.get()); + // Make sure we enumerate over collapsed threads by expanding all. + m_view->ExpandAll(); + m_curHdrIndex = 0; +} + +nsMsgDBView::nsMsgViewHdrEnumerator::~nsMsgViewHdrEnumerator() { + if (m_view) m_view->Close(); +} + +NS_IMETHODIMP +nsMsgDBView::nsMsgViewHdrEnumerator::GetNext(nsIMsgDBHdr** aItem) { + NS_ENSURE_ARG_POINTER(aItem); + + if (m_curHdrIndex >= m_view->GetSize()) return NS_ERROR_FAILURE; + + // Ignore dummy header. We won't have empty groups, so + // we know the view index is good. + if (m_view->m_flags[m_curHdrIndex] & MSG_VIEW_FLAG_DUMMY) ++m_curHdrIndex; + + nsCOMPtr<nsIMsgDBHdr> nextHdr; + + nsresult rv = + m_view->GetMsgHdrForViewIndex(m_curHdrIndex++, getter_AddRefs(nextHdr)); + nextHdr.forget(aItem); + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::nsMsgViewHdrEnumerator::HasMoreElements(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = m_curHdrIndex < m_view->GetSize(); + return NS_OK; +} + +nsresult nsMsgDBView::GetViewEnumerator(nsIMsgEnumerator** enumerator) { + NS_IF_ADDREF(*enumerator = new nsMsgViewHdrEnumerator(this)); + return (*enumerator) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +nsresult nsMsgDBView::GetDBForHeader(nsIMsgDBHdr* msgHdr, nsIMsgDatabase** db) { + nsCOMPtr<nsIMsgFolder> folder; + nsresult rv = msgHdr->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + return folder->GetMsgDatabase(db); +} + +/** + * Determine whether junk commands should be enabled on this view. + * Junk commands are always enabled for mail. For nntp and rss, they + * may be selectively enabled using an inherited folder property. + * + * @param aViewIndex view index of the message to check + * @return true if junk controls should be enabled + */ +bool nsMsgDBView::JunkControlsEnabled(nsMsgViewIndex aViewIndex) { + // For normal mail, junk commands are always enabled. + if (!(mIsNews || mIsRss || mIsXFVirtual)) return true; + + // We need to check per message or folder. + nsCOMPtr<nsIMsgFolder> folder = m_folder; + if (!folder && IsValidIndex(aViewIndex)) + GetFolderForViewIndex(aViewIndex, getter_AddRefs(folder)); + + if (folder) { + // Check if this is a mail message in search folders. + if (mIsXFVirtual) { + nsCOMPtr<nsIMsgIncomingServer> server; + folder->GetServer(getter_AddRefs(server)); + nsAutoCString type; + if (server) server->GetType(type); + + if (!(type.LowerCaseEqualsLiteral("nntp") || + type.LowerCaseEqualsLiteral("rss"))) + return true; + } + + // For rss and news, check the inherited folder property. + nsAutoCString junkEnableOverride; + folder->GetInheritedStringProperty("dobayes.mailnews@mozilla.org#junk", + junkEnableOverride); + if (junkEnableOverride.EqualsLiteral("true")) return true; + } + + return false; +} |