From 302bf1b523012e11b60425d6eee1221ebc2724eb Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Sun, 3 Nov 2019 00:17:46 -0400 Subject: Issue #1258 - Part 1: Import mailnews, ldap, and mork from comm-esr52.9.1 --- mailnews/base/src/nsMsgDBView.cpp | 8066 +++++++++++++++++++++++++++++++++++++ 1 file changed, 8066 insertions(+) create mode 100644 mailnews/base/src/nsMsgDBView.cpp (limited to 'mailnews/base/src/nsMsgDBView.cpp') diff --git a/mailnews/base/src/nsMsgDBView.cpp b/mailnews/base/src/nsMsgDBView.cpp new file mode 100644 index 000000000..f7856dd4d --- /dev/null +++ b/mailnews/base/src/nsMsgDBView.cpp @@ -0,0 +1,8066 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "prmem.h" +#include "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 "nsMsgUtils.h" +#include "nsQuickSort.h" +#include "nsIMsgImapMailFolder.h" +#include "nsImapCore.h" +#include "nsMsgFolderFlags.h" +#include "nsIMsgLocalMailFolder.h" +#include "nsIDOMElement.h" +#include "nsDateTimeFormatCID.h" +#include "nsMsgMimeCID.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIPrefLocalizedString.h" +#include "nsIMsgSearchSession.h" +#include "nsIMsgCopyService.h" +#include "nsMsgBaseCID.h" +#include "nsISpamSettings.h" +#include "nsIMsgAccountManager.h" +#include "nsITreeColumns.h" +#include "nsTextFormatter.h" +#include "nsIMutableArray.h" +#include "nsIMimeConverter.h" +#include "nsMsgMessageFlags.h" +#include "nsIPrompt.h" +#include "nsIWindowWatcher.h" +#include "nsMsgDBCID.h" +#include "nsIMsgFolderNotificationService.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsMemory.h" +#include "nsAlgorithm.h" +#include "nsIAbManager.h" +#include "nsIAbDirectory.h" +#include "nsIAbCard.h" +#include "mozilla/Services.h" +#include "mozilla/Attributes.h" +#include "mozilla/mailnews/MimeHeaderParser.h" +#include "nsTArray.h" +#include + +using namespace mozilla::mailnews; +nsrefcnt nsMsgDBView::gInstanceCount = 0; + +nsIAtom * nsMsgDBView::kJunkMsgAtom = nullptr; +nsIAtom * nsMsgDBView::kNotJunkMsgAtom = nullptr; + +char16_t * nsMsgDBView::kHighestPriorityString = nullptr; +char16_t * nsMsgDBView::kHighPriorityString = nullptr; +char16_t * nsMsgDBView::kLowestPriorityString = nullptr; +char16_t * nsMsgDBView::kLowPriorityString = nullptr; +char16_t * nsMsgDBView::kNormalPriorityString = nullptr; +char16_t * nsMsgDBView::kReadString = nullptr; +char16_t * nsMsgDBView::kRepliedString = nullptr; +char16_t * nsMsgDBView::kForwardedString = nullptr; +char16_t * nsMsgDBView::kNewString = nullptr; + +nsDateFormatSelector nsMsgDBView::m_dateFormatDefault = kDateFormatShort; +nsDateFormatSelector nsMsgDBView::m_dateFormatThisWeek = kDateFormatShort; +nsDateFormatSelector nsMsgDBView::m_dateFormatToday = kDateFormatNone; + +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); + +// this is passed into NS_QuickSort as custom data. +class viewSortInfo +{ +public: + nsMsgDBView *view; + nsIMsgDatabase *db; + bool isSecondarySort; + bool ascendingSort; +}; + + +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_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; + mGoForwardEnabled = false; + mGoBackEnabled = 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; + // initialize any static atoms or unicode strings + if (gInstanceCount == 0) + { + InitializeAtomsAndLiterals(); + InitDisplayFormats(); + } + + InitLabelStrings(); + gInstanceCount++; +} + +void nsMsgDBView::InitializeAtomsAndLiterals() +{ + kJunkMsgAtom = MsgNewAtom("junk").take(); + kNotJunkMsgAtom = MsgNewAtom("notjunk").take(); + + // priority strings + kHighestPriorityString = GetString(u"priorityHighest"); + kHighPriorityString = GetString(u"priorityHigh"); + kLowestPriorityString = GetString(u"priorityLowest"); + kLowPriorityString = GetString(u"priorityLow"); + kNormalPriorityString = GetString(u"priorityNormal"); + + kReadString = GetString(u"read"); + kRepliedString = GetString(u"replied"); + kForwardedString = GetString(u"forwarded"); + kNewString = GetString(u"new"); +} + +nsMsgDBView::~nsMsgDBView() +{ + if (m_db) + m_db->RemoveListener(this); + + gInstanceCount--; + if (gInstanceCount <= 0) + { + NS_IF_RELEASE(kJunkMsgAtom); + NS_IF_RELEASE(kNotJunkMsgAtom); + + NS_Free(kHighestPriorityString); + NS_Free(kHighPriorityString); + NS_Free(kLowestPriorityString); + NS_Free(kLowPriorityString); + NS_Free(kNormalPriorityString); + + NS_Free(kReadString); + NS_Free(kRepliedString); + NS_Free(kForwardedString); + NS_Free(kNewString); + } +} + +nsresult nsMsgDBView::InitLabelStrings() +{ + nsresult rv = NS_OK; + nsCString prefString; + + for(int32_t i = 0; i < PREF_LABELS_MAX; i++) + { + prefString.Assign(PREF_LABELS_DESCRIPTION); + prefString.AppendInt(i + 1); + rv = GetPrefLocalizedString(prefString.get(), mLabelPrefDescriptions[i]); + } + return rv; +} + +// helper function used to fetch strings from the messenger string bundle +char16_t * nsMsgDBView::GetString(const char16_t *aStringName) +{ + nsresult res = NS_ERROR_UNEXPECTED; + char16_t *ptrv = nullptr; + + if (!mMessengerStringBundle) + { + static const char propertyURL[] = MESSENGER_STRING_URL; + nsCOMPtr sBundleService = + mozilla::services::GetStringBundleService(); + if (sBundleService) + res = sBundleService->CreateBundle(propertyURL, getter_AddRefs(mMessengerStringBundle)); + } + + if (mMessengerStringBundle) + res = mMessengerStringBundle->GetStringFromName(aStringName, &ptrv); + + if ( NS_SUCCEEDED(res) && (ptrv) ) + return ptrv; + else + return NS_strdup(aStringName); +} + +// helper function used to fetch localized strings from the prefs +nsresult nsMsgDBView::GetPrefLocalizedString(const char *aPrefName, nsString& aResult) +{ + nsresult rv = NS_OK; + nsCOMPtr prefBranch; + nsCOMPtr 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 addSelectedTextProperty) +{ + // get the top most keyword's color and append that as a property. + nsresult rv; + if (!mTagService) + { + mTagService = do_GetService(NS_MSGTAGSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCString topKey; + rv = mTagService->GetTopKey(keywords, topKey); + NS_ENSURE_SUCCESS(rv, rv); + if (topKey.IsEmpty()) + return NS_OK; + + nsCString color; + rv = mTagService->GetColorForKey(topKey, color); + if (NS_SUCCEEDED(rv) && !color.IsEmpty()) + { + if (addSelectedTextProperty) + { + if (color.EqualsLiteral(LABEL_COLOR_WHITE_STRING)) + properties.AppendLiteral(" lc-black"); + else + properties.AppendLiteral(" lc-white"); + } + color.Replace(0, 1, NS_LITERAL_CSTRING(LABEL_COLOR_STRING)); + properties.AppendASCII(color.get()); + } + return rv; +} + +/////////////////////////////////////////////////////////////////////////// +// nsITreeView Implementation Methods (and helper methods) +/////////////////////////////////////////////////////////////////////////// + +static nsresult GetDisplayNameInAddressBook(const nsACString& emailAddress, + nsAString& displayName) +{ + nsresult rv; + nsCOMPtr abManager(do_GetService("@mozilla.org/abmanager;1", + &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr enumerator; + rv = abManager->GetDirectories(getter_AddRefs(enumerator)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr supports; + nsCOMPtr directory; + nsCOMPtr cardForAddress; + bool hasMore; + + // Scan the addressbook to find out the card of the email address + while (NS_SUCCEEDED(enumerator->HasMoreElements(&hasMore)) && + hasMore && !cardForAddress) + { + rv = enumerator->GetNext(getter_AddRefs(supports)); + NS_ENSURE_SUCCESS(rv, rv); + directory = do_QueryInterface(supports); + if (directory) + { + rv = directory->CardForEmailAddress(emailAddress, + getter_AddRefs(cardForAddress)); + + if (NS_SUCCEEDED(rv) && cardForAddress) + break; // the card is found,so stop looping + } + } + + if (cardForAddress) + { + bool preferDisplayName = true; + cardForAddress->GetPropertyAsBool("PreferDisplayName",&preferDisplayName); + + if (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 verion # + 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 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.get()); +} + +nsresult nsMsgDBView::FetchAuthor(nsIMsgDBHdr * aHdr, nsAString &aSenderString) +{ + nsCString unparsedAuthor; + bool showCondensedAddresses = false; + int32_t currentDisplayNameVersion = 0; + nsCOMPtr prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + + prefs->GetIntPref("mail.displayname.version", ¤tDisplayNameVersion); + prefs->GetBoolPref("mail.showCondensedAddresses", &showCondensedAddresses); + + aHdr->GetStringProperty("sender_name", getter_Copies(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); + + nsCString emailAddress; + nsString name; + ExtractFirstAddress(EncodedHeader(author, headerCharset.get()), 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('>'); + } + } + } + + 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 accountManager( + do_GetService(NS_MSGACCOUNTMANAGER_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr account; + nsCOMPtr server; + if (!accountKey.IsEmpty()) + rv = accountManager->GetAccount(accountKey, getter_AddRefs(account)); + + if (account) + { + account->GetIncomingServer(getter_AddRefs(server)); + } + else + { + nsCOMPtr 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 prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + + prefs->GetIntPref("mail.displayname.version", ¤tDisplayNameVersion); + prefs->GetBoolPref("mail.showCondensedAddresses", &showCondensedAddresses); + + aHdr->GetStringProperty("recipient_names", getter_Copies(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 names; + nsTArray emails; + ExtractAllAddresses(EncodedHeader(unparsedRecipients, headerCharset.get()), + names, UTF16ArrayAdapter<>(emails)); + + uint32_t numAddresses = names.Length(); + + nsCOMPtr enumerator; + nsCOMPtr + 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.Append(NS_LITERAL_STRING(", ")); + + 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; + + if (!mDateFormatter) + mDateFormatter = do_CreateInstance(NS_DATETIMEFORMAT_CONTRACTID); + + NS_ENSURE_TRUE(mDateFormatter, NS_ERROR_FAILURE); + // 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); + + 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. (i.e. 3:15 pm) + // if the message is from the last week, show the day of the week. (i.e. Mon 3:15 pm) + // in all other cases, show the full date (03/19/01 3:15 pm) + + nsDateFormatSelector 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; + } + // 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). + else if (currentTime > dateOfMsg) + { + // 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) + { // yes .... + dateFormat = m_dateFormatThisWeek; + } + } + + if (NS_SUCCEEDED(rv)) + rv = mDateFormatter->FormatPRTime(nullptr /* nsILocale* locale */, + dateFormat, + kTimeFormatNoSeconds, + dateOfMsg, + aDateString); + + 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::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(NS_MSGTAGSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + nsMsgLabelValue label = 0; + + rv = aHdr->GetLabel(&label); + nsCString keywords; + aHdr->GetStringProperty("keywords", getter_Copies(keywords)); + if (label > 0) + { + nsAutoCString labelStr("$label"); + labelStr.Append((char) (label + '0')); + if (keywords.Find(labelStr, CaseInsensitiveCompare) == -1) + { + if (!keywords.IsEmpty()) + keywords.Append(' '); + keywords.Append(labelStr); + } + } + 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 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 thread; + rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv) && thread) + { + uint32_t numChildren; + thread->GetNumChildren(&numChildren); + nsCOMPtr 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(NS_MSGTAGSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsString tags; + nsCString keywords; + aHdr->GetStringProperty("keywords", getter_Copies(keywords)); + + nsMsgLabelValue label = 0; + rv = aHdr->GetLabel(&label); + if (label > 0) + { + nsAutoCString labelStr("$label"); + labelStr.Append((char) (label + '0')); + if (keywords.Find(labelStr, CaseInsensitiveCompare) == -1) + FetchLabel(aHdr, tags); + } + + nsTArray 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; +} + +nsresult nsMsgDBView::FetchLabel(nsIMsgDBHdr *aHdr, nsAString &aLabelString) +{ + nsresult rv = NS_OK; + nsMsgLabelValue label = 0; + + NS_ENSURE_ARG_POINTER(aHdr); + + rv = aHdr->GetLabel(&label); + NS_ENSURE_SUCCESS(rv, rv); + + // we don't care if label is not between 1 and PREF_LABELS_MAX inclusive. + if ((label < 1) || (label > PREF_LABELS_MAX)) + { + aLabelString.Truncate(); + return NS_OK; + } + + // We need to subtract 1 because mLabelPrefDescriptions is 0 based. + aLabelString = mLabelPrefDescriptions[label - 1]; + return NS_OK; +} + +bool nsMsgDBView::IsOutgoingMsg(nsIMsgDBHdr* aHdr) +{ + nsString author; + aHdr->GetMime2DecodedAuthor(author); + + nsCString emailAddress; + nsString name; + ExtractFirstAddress(DecodedHeader(author), name, 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 &aMsgKeyArray) +{ + // we don't do anything on nested Save / Restore calls. + m_saveRestoreSelectionDepth++; + if (m_saveRestoreSelectionDepth != 1) + return NS_OK; + + if (!mTreeSelection || !mTree) + 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; + GetSelectedIndices(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 &aMsgKeyArray) +{ + // we don't do anything on nested Save / Restore calls. + m_saveRestoreSelectionDepth--; + if (m_saveRestoreSelectionDepth) + return NS_OK; + + if (!mTreeSelection) // don't assert. + 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 (mTree) + mTreeSelection->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 (mTree && currentViewPosition != nsMsgViewIndex_None) + 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(nsISimpleEnumerator **enumerator) +{ + return m_db->EnumerateMessages(enumerator); +} + +nsresult nsMsgDBView::CycleThreadedColumn(nsIDOMElement * aElement) +{ + nsAutoString currentView; + + // toggle threaded/unthreaded mode + aElement->GetAttribute(NS_LITERAL_STRING("currentView"), currentView); + if (currentView.EqualsLiteral("threaded")) + aElement->SetAttribute(NS_LITERAL_STRING("currentView"), NS_LITERAL_STRING("unthreaded")); + else + aElement->SetAttribute(NS_LITERAL_STRING("currentView"), NS_LITERAL_STRING("threaded")); + + // i think we need to create a new view and switch it in this circumstance since + // we are toggline between threaded and non threaded mode. + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::IsEditable(int32_t row, nsITreeColumn* col, bool* _retval) +{ + NS_ENSURE_ARG_POINTER(col); + NS_ENSURE_ARG_POINTER(_retval); + //attempt to retreive a custom column handler. If it exists call it and return + const char16_t* colID; + col->GetIdConst(&colID); + + nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID); + + if (colHandler) + { + colHandler->IsEditable(row, col, _retval); + return NS_OK; + } + + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::IsSelectable(int32_t row, nsITreeColumn* col, bool* _retval) +{ + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::SetCellValue(int32_t row, nsITreeColumn* col, const nsAString& value) +{ + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::SetCellText(int32_t row, nsITreeColumn* 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) +{ + *aSelection = mTreeSelection; + NS_IF_ADDREF(*aSelection); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::SetSelection(nsITreeSelection * aSelection) +{ + mTreeSelection = aSelection; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::ReloadMessageWithAllParts() +{ + if (m_currentlyDisplayedMsgUri.IsEmpty() || mSuppressMsgDisplay) + return NS_OK; + + nsAutoCString forceAllParts(m_currentlyDisplayedMsgUri); + forceAllParts += (forceAllParts.FindChar('?') == kNotFound) ? '?' : '&'; + forceAllParts.AppendLiteral("fetchCompleteMessage=true"); + nsCOMPtr messenger (do_QueryReferent(mMessengerWeak)); + NS_ENSURE_TRUE(messenger, NS_ERROR_FAILURE); + + nsresult rv = messenger->OpenURL(forceAllParts); + NS_ENSURE_SUCCESS(rv, rv); + + UpdateDisplayMessage(m_currentlyDisplayedViewIndex); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::ReloadMessage() +{ + if (m_currentlyDisplayedMsgUri.IsEmpty() || mSuppressMsgDisplay) + return NS_OK; + nsCOMPtr messenger (do_QueryReferent(mMessengerWeak)); + NS_ENSURE_TRUE(messenger, NS_ERROR_FAILURE); + + nsresult rv = messenger->OpenURL(m_currentlyDisplayedMsgUri); + NS_ENSURE_SUCCESS(rv, rv); + + UpdateDisplayMessage(m_currentlyDisplayedViewIndex); + return NS_OK; +} + +nsresult nsMsgDBView::UpdateDisplayMessage(nsMsgViewIndex viewPosition) +{ + nsresult rv; + if (mCommandUpdater) + { + // get the subject and the folder for the message and inform the front end that + // we changed the message we are currently displaying. + if (viewPosition != nsMsgViewIndex_None) + { + nsCOMPtr msgHdr; + rv = GetMsgHdrForViewIndex(viewPosition, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv,rv); + + nsString subject; + FetchSubject(msgHdr, m_flags[viewPosition], subject); + + nsCString keywords; + rv = msgHdr->GetStringProperty("keywords", getter_Copies(keywords)); + NS_ENSURE_SUCCESS(rv,rv); + + nsCOMPtr folder = m_viewFolder ? m_viewFolder : m_folder; + + mCommandUpdater->DisplayMessageChanged(folder, subject, keywords); + + if (folder) + { + rv = folder->SetLastMessageLoaded(m_keys[viewPosition]); + NS_ENSURE_SUCCESS(rv,rv); + } + } // if view position is valid + } // if we have an updater + return NS_OK; +} + +// given a msg key, we will load the message for it. +NS_IMETHODIMP nsMsgDBView::LoadMessageByMsgKey(nsMsgKey aMsgKey) +{ + return LoadMessageByViewIndex(FindKey(aMsgKey, false)); +} + +NS_IMETHODIMP nsMsgDBView::LoadMessageByViewIndex(nsMsgViewIndex aViewIndex) +{ + NS_ASSERTION(aViewIndex != nsMsgViewIndex_None,"trying to load nsMsgViewIndex_None"); + if (aViewIndex == nsMsgViewIndex_None) return NS_ERROR_UNEXPECTED; + + nsCString uri; + nsresult rv = GetURIForViewIndex(aViewIndex, uri); + if (!mSuppressMsgDisplay && !m_currentlyDisplayedMsgUri.Equals(uri)) + { + NS_ENSURE_SUCCESS(rv,rv); + nsCOMPtr messenger (do_QueryReferent(mMessengerWeak)); + NS_ENSURE_TRUE(messenger, NS_ERROR_FAILURE); + messenger->OpenURL(uri); + m_currentlyDisplayedMsgKey = m_keys[aViewIndex]; + m_currentlyDisplayedMsgUri = uri; + m_currentlyDisplayedViewIndex = aViewIndex; + UpdateDisplayMessage(m_currentlyDisplayedViewIndex); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::LoadMessageByUrl(const char *aUrl) +{ + NS_ASSERTION(aUrl, "trying to load a null url"); + if (!mSuppressMsgDisplay) + { + nsresult rv; + nsCOMPtr messenger (do_QueryReferent(mMessengerWeak, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + messenger->LoadURL(NULL, nsDependentCString(aUrl)); + m_currentlyDisplayedMsgKey = nsMsgKey_None; + m_currentlyDisplayedMsgUri = aUrl; + m_currentlyDisplayedViewIndex = nsMsgViewIndex_None; + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::SelectionChanged() +{ + // 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; + + uint32_t numSelected = 0; + + nsMsgViewIndexArray selection; + GetSelectedIndices(selection); + nsMsgViewIndex *indices = selection.Elements(); + numSelected = selection.Length(); + + bool commandsNeedDisablingBecauseOfSelection = false; + + if(indices) + { + if (WeAreOffline()) + commandsNeedDisablingBecauseOfSelection = !OfflineMsgSelected(indices, numSelected); + if (!NonDummyMsgSelected(indices, numSelected)) + 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 + if (mCommandUpdater) + { + mCommandUpdater->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 && + (numSelected > 1 || (numSelected == 1 && + m_flags[indices[0]] & nsMsgMessageFlags::Elided && + OperateOnMsgsInCollapsedThreads()))) + mSummarizeFailed = true; + } + + bool summaryStateChanged = selectionSummarized != mSelectionSummarized; + + mSelectionSummarized = selectionSummarized; + // if only one item is selected then we want to display a message + if (mTreeSelection && numSelected == 1 && !selectionSummarized) + { + int32_t startRange; + int32_t endRange; + nsresult rv = mTreeSelection->GetRangeAt(0, &startRange, &endRange); + NS_ENSURE_SUCCESS(rv, NS_OK); // tree doesn't care if we failed + + if (startRange >= 0 && startRange == endRange && + uint32_t(startRange) < GetSize()) + { + if (!mRemovingRow) + { + if (!mSuppressMsgDisplay) + LoadMessageByViewIndex(startRange); + else + UpdateDisplayMessage(startRange); + } + } + else + numSelected = 0; // selection seems bogus, so set to 0. + } + else { + // 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 or not. + + // 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. + + bool enableGoForward = false; + bool enableGoBack = false; + + NavigateStatus(nsMsgNavigationType::forward, &enableGoForward); + NavigateStatus(nsMsgNavigationType::back, &enableGoBack); + if (!summaryStateChanged && + (numSelected == mNumSelectedRows || + (numSelected > 1 && mNumSelectedRows > 1)) && (commandsNeedDisablingBecauseOfSelection == mCommandsNeedDisablingBecauseOfSelection) + && enableGoForward == mGoForwardEnabled && enableGoBack == mGoBackEnabled) + { + } + // don't update commands if we're suppressing them, or if we're removing rows, unless it was the last row. + else if (!mSuppressCommandUpdating && mCommandUpdater && (!mRemovingRow || GetSize() == 0)) // o.t. push an updated + { + mCommandUpdater->UpdateCommandStatus(); + } + + mCommandsNeedDisablingBecauseOfSelection = commandsNeedDisablingBecauseOfSelection; + mGoForwardEnabled = enableGoForward; + mGoBackEnabled = enableGoBack; + mNumSelectedRows = numSelected; + return NS_OK; +} + +nsresult nsMsgDBView::GetSelectedIndices(nsMsgViewIndexArray& selection) +{ + if (mTreeSelection) + { + int32_t viewSize = GetSize(); + int32_t count; + mTreeSelection->GetCount(&count); + selection.SetLength(count); + count = 0; + 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++) + selection[count++] = rangeIndex; + } + } + NS_ASSERTION(selection.Length() == uint32_t(count), "selection count is wrong"); + selection.SetLength(count); + } + 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) + selection.AppendElement(viewIndex); + } + 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 msgHdr; + nsresult rv = NS_OK; + + rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr)); + + if (NS_FAILED(rv) || !msgHdr) { + ClearHdrCache(); + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + nsCString keywordProperty; + FetchRowKeywords(index, msgHdr, keywordProperty); + if (!keywordProperty.IsEmpty()) + AppendKeywordProperties(keywordProperty, properties, false); + + // 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); + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::GetColumnProperties(nsITreeColumn* col, nsAString& properties) +{ + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::GetCellProperties(int32_t aRow, nsITreeColumn *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 msgHdr; + nsresult rv = NS_OK; + + rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr)); + + if (NS_FAILED(rv) || !msgHdr) + { + ClearHdrCache(); + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + const char16_t* colID; + col->GetIdConst(&colID); + 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::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 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", getter_Copies(imageSize)); + if (!imageSize.IsEmpty()) + properties.AppendLiteral(" hasimage"); + + nsCString junkScoreStr; + msgHdr->GetStringProperty("junkscore", getter_Copies(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); + if (!keywords.IsEmpty()) + AppendKeywordProperties(keywords, properties, true); + + // 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", getter_Copies(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 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) + { + *msgHdr = m_cachedHdr; + NS_IF_ADDREF(*msgHdr); + } + 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, nsITreeColumn* aCol, nsAString& aValue) +{ + NS_ENSURE_ARG_POINTER(aCol); + //attempt to retreive a custom column handler. If it exists call it and return + const char16_t* colID; + aCol->GetIdConst(&colID); + + nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID); + + if (colHandler) + { + colHandler->GetImageSrc(aRow, aCol, aValue); + return NS_OK; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetProgressMode(int32_t aRow, nsITreeColumn* aCol, int32_t* _retval) +{ + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::GetCellValue(int32_t aRow, nsITreeColumn* aCol, nsAString& aValue) +{ + if (!IsValidIndex(aRow)) + return NS_MSG_INVALID_DBVIEW_INDEX; + + nsCOMPtr msgHdr; + nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr)); + + if (NS_FAILED(rv) || !msgHdr) + { + ClearHdrCache(); + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + const char16_t* colID; + aCol->GetIdConst(&colID); + + uint32_t flags; + msgHdr->GetFlags(&flags); + + aValue.Truncate(); + // 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[0]) + { + case 'a': // attachment column + if (flags & nsMsgMessageFlags::Attachment) { + nsString tmp_str; + tmp_str.Adopt(GetString(u"messageHasAttachment")); + aValue.Assign(tmp_str); + } + break; + case 'f': // flagged (starred) column + if (flags & nsMsgMessageFlags::Marked) { + nsString tmp_str; + tmp_str.Adopt(GetString(u"messageHasFlag")); + aValue.Assign(tmp_str); + } + break; + case 'j': // junk column + if (JunkControlsEnabled(aRow)) + { + nsCString junkScoreStr; + msgHdr->GetStringProperty("junkscore", getter_Copies(junkScoreStr)); + // Only need to assing 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[1] == 'h' && (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) + { // thread column + bool isContainer, isContainerEmpty, isContainerOpen; + IsContainer(aRow, &isContainer); + if (isContainer) + { + IsContainerEmpty(aRow, &isContainerEmpty); + if (!isContainerEmpty) + { + nsString tmp_str; + + IsContainerOpen(aRow, &isContainerOpen); + tmp_str.Adopt(GetString(isContainerOpen ? + u"messageExpanded" : + u"messageCollapsed")); + aValue.Assign(tmp_str); + } + } + } + break; + case 'u': // read/unread column + if (!(flags & nsMsgMessageFlags::Read)) { + nsString tmp_str; + tmp_str.Adopt(GetString(u"messageUnread")); + aValue.Assign(tmp_str); + } + 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); +} + +//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; + } + + return NS_ERROR_FAILURE; //can't remove a column that isn't currently custom handled +} + +//TODO: NS_ENSURE_SUCCESS +nsIMsgCustomColumnHandler* nsMsgDBView::GetCurColumnHandler() +{ + return GetColumnHandler(m_curCustomColumn.get()); +} + +NS_IMETHODIMP nsMsgDBView::SetCurCustomColumn(const nsAString& aColID) +{ + m_curCustomColumn = aColID; + + if (m_viewFolder) + { + nsCOMPtr db; + nsCOMPtr 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 char16_t *colID) +{ + size_t index = m_customColumnHandlerIDs.IndexOf(nsDependentString(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.get())); + 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; +} + +NS_IMETHODIMP nsMsgDBView::GetCellText(int32_t aRow, nsITreeColumn* aCol, nsAString& aValue) +{ + const char16_t* colID; + aCol->GetIdConst(&colID); + + if (!IsValidIndex(aRow)) + return NS_MSG_INVALID_DBVIEW_INDEX; + + aValue.Truncate(); + + //attempt to retreive 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 char16_t *aColumnName, + nsAString &aValue) +{ + nsCOMPtr msgHdr; + nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr)); + + if (NS_FAILED(rv) || !msgHdr) + { + ClearHdrCache(); + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + nsCOMPtr thread; + + switch (aColumnName[0]) + { + case 's': + if (aColumnName[1] == 'u') // subject + rv = FetchSubject(msgHdr, m_flags[aRow], aValue); + else if (aColumnName[1] == 'e') // sender + rv = FetchAuthor(msgHdr, aValue); + else if (aColumnName[1] == 'i') // size + rv = FetchSize(msgHdr, aValue); + else if (aColumnName[1] == 't') // status + { + uint32_t flags; + msgHdr->GetFlags(&flags); + rv = FetchStatus(flags, aValue); + } + break; + case 'r': + if (aColumnName[3] == 'i') // recipient + rv = FetchRecipients(msgHdr, aValue); + else if (aColumnName[3] == 'e') // received + rv = FetchDate(msgHdr, aValue, true); + break; + case 'd': // date + rv = FetchDate(msgHdr, aValue); + break; + case 'c': // correspondent + if (IsOutgoingMsg(msgHdr)) + rv = FetchRecipients(msgHdr, aValue); + else + rv = FetchAuthor(msgHdr, aValue); + break; + case 'p': // priority + rv = FetchPriority(msgHdr, aValue); + break; + case 'a': // account + if (aColumnName[1] == 'c') // account + rv = FetchAccount(msgHdr, aValue); + break; + case 't': + // total msgs in thread column + if (aColumnName[1] == 'o' && (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[1] == 'a') // tags + { + rv = FetchTags(msgHdr, aValue); + } + break; + case 'u': + // unread msgs in thread col + if (aColumnName[6] == 'C' && (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': + { + nsCString junkScoreStr; + msgHdr->GetStringProperty("junkscore", getter_Copies(junkScoreStr)); + CopyASCIItoUTF16(junkScoreStr, aValue); + } + break; + case 'i': // id + { + nsAutoString keyString; + nsMsgKey key; + msgHdr->GetMessageKey(&key); + keyString.AppendInt((int64_t)key); + aValue.Assign(keyString); + } + break; + default: + break; + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::SetTree(nsITreeBoxObject *tree) +{ + mTree = 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(nsITreeColumn* 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, nsITreeColumn* col) +{ + if (!IsValidIndex(row)) + return NS_MSG_INVALID_DBVIEW_INDEX; + + const char16_t* colID; + col->GetIdConst(&colID); + + //attempt to retreive 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; + + switch (colID[0]) + { + case 'u': // unreadButtonColHeader + if (colID[6] == 'B') + ApplyCommandToIndices(nsMsgViewCommandType::toggleMessageRead, (nsMsgViewIndex *) &row, 1); + break; + case 't': // tag cell, threaded cell or total cell + if (colID[1] == 'h') + { + ExpandAndSelectThreadByIndex(row, false); + } + else if (colID[1] == 'a') + { + // ### 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': // flagged column + // toggle the flagged status of the element at row. + if (m_flags[row] & nsMsgMessageFlags::Marked) + ApplyCommandToIndices(nsMsgViewCommandType::unflagMessages, (nsMsgViewIndex *) &row, 1); + else + ApplyCommandToIndices(nsMsgViewCommandType::flagMessages, (nsMsgViewIndex *) &row, 1); + break; + case 'j': // junkStatus column + { + if (!JunkControlsEnabled(row)) + return NS_OK; + + nsCOMPtr msgHdr; + + nsresult rv = GetMsgHdrForViewIndex(row, getter_AddRefs(msgHdr)); + if (NS_SUCCEEDED(rv) && msgHdr) + { + nsCString junkScoreStr; + rv = msgHdr->GetStringProperty("junkscore", getter_Copies(junkScoreStr)); + if (junkScoreStr.IsEmpty() || (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_HAM_SCORE)) + ApplyCommandToIndices(nsMsgViewCommandType::junk, (nsMsgViewIndex *) &row, 1); + else + ApplyCommandToIndices(nsMsgViewCommandType::unjunk, (nsMsgViewIndex *) &row, 1); + + NS_ASSERTION(NS_SUCCEEDED(rv), "Converting junkScore to integer failed."); + } + } + break; + default: + break; + + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::PerformAction(const char16_t *action) +{ + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::PerformActionOnRow(const char16_t *action, int32_t row) +{ + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::PerformActionOnCell(const char16_t *action, int32_t row, nsITreeColumn* col) +{ + 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 accountManager = + do_GetService(NS_MSGACCOUNTMANAGER_CONTRACTID, &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 folderInfo; + rv = folder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(m_db)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr msgDBService = do_GetService(NS_MSGDB_SERVICE_CONTRACTID, &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 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 = MsgLowerCaseEqualsLiteral(type, "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 && MsgLowerCaseEqualsLiteral(type, "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 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); + } + } + + nsCOMPtr identities; + rv = accountManager->GetAllIdentities(getter_AddRefs(identities)); + if (!identities) + return rv; + + uint32_t count; + identities->GetLength(&count); + for (uint32_t i = 0; i < count; i++) + { + nsCOMPtr identity(do_QueryElementAt(identities, i)); + if (!identity) + continue; + + nsCString email; + identity->GetEmail(email); + if (!email.IsEmpty()) + mEmails.PutEntry(email); + + identity->GetReplyTo(email); + if (!email.IsEmpty()) + 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 get determine 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 + if (mJunkHdrs) + mJunkHdrs->Clear(); + + // this needs to happen after we remove all the keys, since RowCountChanged() will call our GetRowCount() + if (mTree) + mTree->RowCountChanged(0, -oldSize); + + ClearHdrCache(); + if (m_db) + { + m_db->RemoveListener(this); + m_db = nullptr; + } + if (m_folder) + { + nsresult rv; + nsCOMPtr msgDBService = do_GetService(NS_MSGDB_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + msgDBService->UnregisterPendingListener(this); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::OpenWithHdrs(nsISimpleEnumerator *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 = 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) +{ + uint32_t numSelected = 0; + GetNumSelected(&numSelected); + + bool forceDisplay = false; + if (mSuppressMsgDisplay && !aSuppressDisplay && numSelected == 1) + forceDisplay = true; + + mSuppressMsgDisplay = aSuppressDisplay; + if (forceDisplay) + { + // get the view indexfor the currently selected message + nsMsgViewIndex viewIndex; + nsresult rv = GetViewIndexForFirstSelectedMsg(&viewIndex); + if (NS_SUCCEEDED(rv) && viewIndex != nsMsgViewIndex_None) + LoadMessageByViewIndex(viewIndex); + } + + 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; +} + +NS_IMETHODIMP nsMsgDBView::GetIndicesForSelection(uint32_t *length, nsMsgViewIndex **indices) +{ + NS_ENSURE_ARG_POINTER(length); + *length = 0; + NS_ENSURE_ARG_POINTER(indices); + *indices = nullptr; + + nsMsgViewIndexArray selection; + GetSelectedIndices(selection); + uint32_t numIndices = selection.Length(); + if (!numIndices) return NS_OK; + *length = numIndices; + + uint32_t datalen = numIndices * sizeof(nsMsgViewIndex); + *indices = (nsMsgViewIndex *)NS_Alloc(datalen); + if (!*indices) return NS_ERROR_OUT_OF_MEMORY; + memcpy(*indices, selection.Elements(), datalen); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::GetSelectedMsgHdrs(uint32_t *aLength, nsIMsgDBHdr ***aResult) +{ + nsresult rv; + + NS_ENSURE_ARG_POINTER(aLength); + *aLength = 0; + + NS_ENSURE_ARG_POINTER(aResult); + *aResult = nullptr; + + nsMsgViewIndexArray selection; + GetSelectedIndices(selection); + uint32_t numIndices = selection.Length(); + if (!numIndices) return NS_OK; + + nsCOMPtr messages(do_CreateInstance(NS_ARRAY_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetHeadersFromSelection(selection.Elements(), numIndices, messages); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t numMsgsSelected; + messages->GetLength(&numMsgsSelected); + + nsIMsgDBHdr **headers = static_cast(NS_Alloc( + sizeof(nsIMsgDBHdr*) * numMsgsSelected)); + for (uint32_t i = 0; i < numMsgsSelected; i++) + { + nsCOMPtr msgHdr = do_QueryElementAt(messages, i, &rv); + NS_ENSURE_SUCCESS(rv, rv); + msgHdr.forget(&headers[i]); // Already AddRefed + } + + *aLength = numMsgsSelected; + *aResult = headers; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::GetMsgHdrsForSelection(nsIMutableArray **aResult) +{ + nsMsgViewIndexArray selection; + GetSelectedIndices(selection); + uint32_t numIndices = selection.Length(); + + nsresult rv; + nsCOMPtr messages(do_CreateInstance(NS_ARRAY_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetHeadersFromSelection(selection.Elements(), numIndices, messages); + NS_ENSURE_SUCCESS(rv, rv); + messages.forget(aResult); + return rv; +} + +NS_IMETHODIMP nsMsgDBView::GetURIsForSelection(uint32_t *length, char ***uris) +{ + nsresult rv = NS_OK; + + NS_ENSURE_ARG_POINTER(length); + *length = 0; + + NS_ENSURE_ARG_POINTER(uris); + *uris = nullptr; + + nsMsgViewIndexArray selection; + GetSelectedIndices(selection); + uint32_t numIndices = selection.Length(); + if (!numIndices) return NS_OK; + + nsCOMPtr messages(do_CreateInstance(NS_ARRAY_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetHeadersFromSelection(selection.Elements(), numIndices, messages); + NS_ENSURE_SUCCESS(rv, rv); + messages->GetLength(length); + uint32_t numMsgsSelected = *length; + + char **outArray, **next; + next = outArray = (char **)moz_xmalloc(numMsgsSelected * sizeof(char *)); + if (!outArray) return NS_ERROR_OUT_OF_MEMORY; + for (uint32_t i = 0; i < numMsgsSelected; i++) + { + nsCString tmpUri; + nsCOMPtr msgHdr = do_QueryElementAt(messages, i, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr folder; + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFolder(getter_AddRefs(folder)); + rv = GenerateURIForMsgKey(msgKey, folder, tmpUri); + NS_ENSURE_SUCCESS(rv,rv); + *next = ToNewCString(tmpUri); + if (!*next) + return NS_ERROR_OUT_OF_MEMORY; + + next++; + } + + *uris = outArray; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::GetURIForViewIndex(nsMsgViewIndex index, nsACString &result) +{ + nsresult rv; + nsCOMPtr 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; + + GetSelectedIndices(selection); + + nsMsgViewIndex *indices = selection.Elements(); + int32_t numIndices = selection.Length(); + + nsresult rv = NS_OK; + switch (command) { + case nsMsgViewCommandType::copyMessages: + case nsMsgViewCommandType::moveMessages: + NoteStartChange(nsMsgViewNotificationCode::none, 0, 0); + rv = ApplyCommandToIndicesWithFolder(command, indices, numIndices, destFolder); + NoteEndChange(nsMsgViewNotificationCode::none, 0, 0); + break; + default: + NS_ASSERTION(false, "invalid command type"); + rv = NS_ERROR_UNEXPECTED; + break; + } + return rv; + +} + +NS_IMETHODIMP nsMsgDBView::DoCommand(nsMsgViewCommandTypeValue command) +{ + nsMsgViewIndexArray selection; + + GetSelectedIndices(selection); + + nsMsgViewIndex *indices = selection.Elements(); + int32_t numIndices = selection.Length(); + nsCOMPtr msgWindow(do_QueryReferent(mMsgWindowWeak)); + + nsresult rv = NS_OK; + switch (command) + { + case nsMsgViewCommandType::downloadSelectedForOffline: + return DownloadForOffline(msgWindow, indices, numIndices); + 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: + NoteStartChange(nsMsgViewNotificationCode::none, 0, 0); + rv = ApplyCommandToIndices(command, indices, numIndices); + NoteEndChange(nsMsgViewNotificationCode::none, 0, 0); + 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(); + } + break; + case nsMsgViewCommandType::selectThread: + rv = ExpandAndSelectThread(); + break; + case nsMsgViewCommandType::selectFlagged: + if (!mTreeSelection) + rv = NS_ERROR_UNEXPECTED; + else + { + mTreeSelection->SetSelectEventsSuppressed(true); + mTreeSelection->ClearSelection(); + // XXX ExpandAll? + nsMsgViewIndex numIndices = GetSize(); + for (nsMsgViewIndex 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(); + } + break; + case nsMsgViewCommandType::toggleThreadWatched: + rv = ToggleWatched(indices, numIndices); + break; + case nsMsgViewCommandType::expandAll: + rv = ExpandAll(); + m_viewFlags |= nsMsgViewFlagsType::kExpandAll; + SetViewFlags(m_viewFlags); + if (mTree) + mTree->Invalidate(); + break; + case nsMsgViewCommandType::collapseAll: + rv = CollapseAll(); + m_viewFlags &= ~nsMsgViewFlagsType::kExpandAll; + SetViewFlags(m_viewFlags); + if(mTree) + mTree->Invalidate(); + break; + default: + NS_ASSERTION(false, "invalid command type"); + rv = NS_ERROR_UNEXPECTED; + break; + } + return rv; +} + +bool nsMsgDBView::ServerSupportsFilterAfterTheFact() +{ + if (!m_folder) // cross folder virtual folders might not have a folder set. + return false; + + nsCOMPtr server; + nsresult rv = m_folder->GetServer(getter_AddRefs(server)); + if (NS_FAILED(rv)) + return false; // unexpected + + // 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); + if (NS_FAILED(rv)) + return false; // unexpected + + 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; + GetSelectedIndices(selection); + int32_t numIndices = selection.Length(); + nsMsgViewIndex *indices = selection.Elements(); + // 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(indices, numIndices); + 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 && numIndices && JunkControlsEnabled(selection[0]); + break; + case nsMsgViewCommandType::cmdRequiringMsgBody: + *selectable_p = haveSelection && (!WeAreOffline() || OfflineMsgSelected(indices, numIndices)); + 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, + nsIMutableArray *messageArray) +{ + nsCOMPtr msgHdr; + nsCOMPtr 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 msgHdr; + rv = thread->GetChildHdrAt(i, getter_AddRefs(msgHdr)); + if (!msgHdr) + continue; + rv = messageArray->AppendElement(msgHdr, false); + } + return rv; +} + +bool nsMsgDBView::OperateOnMsgsInCollapsedThreads() +{ + if (mTreeSelection) + { + nsCOMPtr selTree; + mTreeSelection->GetTree(getter_AddRefs(selTree)); + // no tree means stand-alone message window + if (!selTree) + return false; + } + + nsresult rv = NS_OK; + nsCOMPtr 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(uint32_t *indices, + uint32_t numIndices, + nsIMutableArray *messageArray) +{ + nsresult rv = NS_OK; + + // Don't include collapsed messages if the front end failed to summarize + // the selection. + bool includeCollapsedMsgs = OperateOnMsgsInCollapsedThreads() && + !mSummarizeFailed; + + for (uint32_t index = 0; + index < (nsMsgViewIndex) numIndices && NS_SUCCEEDED(rv); index++) + { + nsMsgViewIndex viewIndex = indices[index]; + 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, messageArray); + continue; + } + nsCOMPtr msgHdr; + rv = GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr)); + if (NS_SUCCEEDED(rv) && msgHdr) + { + rv = messageArray->AppendElement(msgHdr, false); + if (NS_SUCCEEDED(rv) && includeCollapsedMsgs && + viewIndexFlags & nsMsgMessageFlags::Elided && + viewIndexFlags & MSG_VIEW_FLAG_HASCHILDREN && + m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) + { + rv = ListCollapsedChildren(viewIndex, messageArray); + } + } + } + return rv; +} + +nsresult +nsMsgDBView::CopyMessages(nsIMsgWindow *window, nsMsgViewIndex *indices, int32_t numIndices, 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); + nsCOMPtr messageArray(do_CreateInstance(NS_ARRAY_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetHeadersFromSelection(indices, numIndices, messageArray); + NS_ENSURE_SUCCESS(rv, rv); + + m_deletingRows = isMove && mDeleteModel != nsMsgImapDeleteModels::IMAPDelete; + if (m_deletingRows) + mIndicesToNoteChange.AppendElements(indices, numIndices); + + nsCOMPtr copyService = do_GetService(NS_MSGCOPYSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + return copyService->CopyMessages(m_folder /* source folder */, messageArray, destFolder, isMove, nullptr /* listener */, window, true /*allowUndo*/); +} + +nsresult +nsMsgDBView::ApplyCommandToIndicesWithFolder(nsMsgViewCommandTypeValue command, nsMsgViewIndex* indices, + int32_t numIndices, nsIMsgFolder *destFolder) +{ + nsresult rv = NS_OK; + NS_ENSURE_ARG_POINTER(destFolder); + + nsCOMPtr 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, indices, numIndices, 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, indices, numIndices, true /* isMove */, destFolder); + break; + default: + NS_ASSERTION(false, "unhandled command"); + rv = NS_ERROR_UNEXPECTED; + break; + } + return rv; +} + +nsresult +nsMsgDBView::ApplyCommandToIndices(nsMsgViewCommandTypeValue command, nsMsgViewIndex* indices, + int32_t numIndices) +{ + NS_ASSERTION(numIndices >= 0, "nsMsgDBView::ApplyCommandToIndices(): " + "numIndices is negative!"); + + if (numIndices == 0) + return NS_OK; // return quietly, just in case + + nsCOMPtr folder; + nsresult rv = GetFolderForViewIndex(indices[0], getter_AddRefs(folder)); + nsCOMPtr msgWindow(do_QueryReferent(mMsgWindowWeak)); + if (command == nsMsgViewCommandType::deleteMsg) + return DeleteMessages(msgWindow, indices, numIndices, false); + if (command == nsMsgViewCommandType::deleteNoTrash) + return DeleteMessages(msgWindow, indices, numIndices, true); + + nsTArray imapUids; + nsCOMPtr imapFolder = do_QueryInterface(folder); + bool thisIsImapFolder = (imapFolder != nullptr); + nsCOMPtr 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 server; + rv = folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr filterPlugin; + rv = server->GetSpamFilterPlugin(getter_AddRefs(filterPlugin)); + NS_ENSURE_SUCCESS(rv, rv); + + junkPlugin = do_QueryInterface(filterPlugin, &rv); + NS_ENSURE_SUCCESS(rv, rv); + if (!mJunkHdrs) + { + mJunkHdrs = do_CreateInstance(NS_ARRAY_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv,rv); + } + } + + folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications, false, true /*dbBatching*/); + + // no sense going through the code that handles messages in collasped threads + // for mark thread read. + if (command == nsMsgViewCommandType::markThreadRead) + { + for (int32_t index = 0; index < numIndices; index++) + SetThreadOfMsgReadByIndex(indices[index], imapUids, true); + } + else + { + // Turn the selection into an array of msg hdrs. This may include messages + // in collapsed threads + uint32_t length; + nsCOMPtr messages(do_CreateInstance(NS_ARRAY_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetHeadersFromSelection(indices, numIndices, messages); + NS_ENSURE_SUCCESS(rv, rv); + messages->GetLength(&length); + + if (thisIsImapFolder) + imapUids.SetLength(length); + + for (uint32_t i = 0; i < length; i++) + { + nsMsgKey msgKey; + nsCOMPtr msgHdr = do_QueryElementAt(messages, i); + msgHdr->GetMessageKey(&msgKey); + if (thisIsImapFolder) + imapUids[i] = msgKey; + + switch (command) + { + case nsMsgViewCommandType::junk: + mNumMessagesRemainingInBatch++; + mJunkHdrs->AppendElement(msgHdr, false); + rv = SetMsgHdrJunkStatus(junkPlugin.get(), msgHdr, + nsIJunkMailPlugin::JUNK); + break; + case nsMsgViewCommandType::unjunk: + mNumMessagesRemainingInBatch++; + mJunkHdrs->AppendElement(msgHdr, false); + 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: + break; // this is completely handled in the code below. + default: + NS_ERROR("unhandled command"); + break; + } + } + + switch (command) + { + case nsMsgViewCommandType::toggleMessageRead: + { + nsCOMPtr msgHdr = do_QueryElementAt(messages, 0); + if (!msgHdr) + break; + uint32_t msgFlags; + msgHdr->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 + notifier(do_GetService(NS_MSGNOTIFICATIONSERVICE_CONTRACTID)); + if (notifier) + notifier->NotifyItemEvent(messages, + NS_LITERAL_CSTRING("JunkStatusChanged"), + (command == nsMsgViewCommandType::junk) ? + kJunkMsgAtom : kNotJunkMsgAtom); + } + } + + folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications, true, true /*dbBatching*/); + + if (thisIsImapFolder) + { + imapMessageFlagsType flags = kNoImapMsgFlag; + bool addFlags = false; + nsCOMPtr 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, + NS_LITERAL_CSTRING("Junk"), + NS_LITERAL_CSTRING("NonJunk"), + imapUids.Elements(), imapUids.Length(), + nullptr); + case nsMsgViewCommandType::unjunk: + { + nsCOMPtr msgHdr; + GetHdrForFirstSelectedMessage(getter_AddRefs(msgHdr)); + uint32_t msgFlags = 0; + if (msgHdr) + msgHdr->GetFlags(&msgFlags); + if (msgFlags & nsMsgMessageFlags::IMAPDeleted) + imapFolder->StoreImapFlags(kImapMsgDeletedFlag, false, + imapUids.Elements(), + imapUids.Length(), nullptr); + return imapFolder->StoreCustomKeywords(msgWindow, + NS_LITERAL_CSTRING("NonJunk"), + NS_LITERAL_CSTRING("Junk"), + imapUids.Elements(), imapUids.Length(), + nullptr); + } + + default: + break; + } + + if (flags != kNoImapMsgFlag) // can't get here without thisIsImapThreadPane == TRUE + imapFolder->StoreImapFlags(flags, addFlags, imapUids.Elements(), imapUids.Length(), 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() + if (!m_deletingRows) + NoteChange(index, -1, nsMsgViewNotificationCode::insertOrDelete); // an example where view is not the listener - D&D messages + + return NS_OK; +} + +nsresult nsMsgDBView::DeleteMessages(nsIMsgWindow *window, nsMsgViewIndex *indices, int32_t numIndices, bool deleteStorage) +{ + if (m_deletingRows) + { + NS_WARNING("Last delete did not complete"); + return NS_OK; + } + nsresult rv; + nsCOMPtr messageArray(do_CreateInstance(NS_ARRAY_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetHeadersFromSelection(indices, numIndices, messageArray); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t numMsgs; + messageArray->GetLength(&numMsgs); + + 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 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 && (uint32_t(numIndices) != numMsgs)) + { + 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 dialog; + + nsCOMPtr wwatch(do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = wwatch->GetNewPrompter(0, getter_AddRefs(dialog)); + NS_ENSURE_SUCCESS(rv, rv); + bool dontAsk = false; // "Don't ask..." - unchecked by default. + int32_t buttonPressed = 0; + + nsString dialogTitle; + nsString confirmString; + nsString checkboxText; + nsString buttonApplyNowText; + dialogTitle.Adopt(GetString(u"confirmMsgDelete.title")); + checkboxText.Adopt(GetString(u"confirmMsgDelete.dontAsk.label")); + buttonApplyNowText.Adopt(GetString(u"confirmMsgDelete.delete.label")); + + confirmString.Adopt(GetString(warningName.get())); + + 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 (mDeleteModel != nsMsgImapDeleteModels::IMAPDelete) + m_deletingRows = true; + + if (m_deletingRows) + mIndicesToNoteChange.AppendElements(indices, numIndices); + + rv = m_folder->DeleteMessages(messageArray, window, deleteStorage, false, nullptr, true /*allow Undo*/ ); + if (NS_FAILED(rv)) + m_deletingRows = false; + return rv; +} + +nsresult nsMsgDBView::DownloadForOffline(nsIMsgWindow *window, nsMsgViewIndex *indices, int32_t numIndices) +{ + nsresult rv = NS_OK; + nsCOMPtr messageArray(do_CreateInstance(NS_ARRAY_CONTRACTID)); + for (nsMsgViewIndex index = 0; index < (nsMsgViewIndex) numIndices; index++) + { + nsMsgKey key = m_keys[indices[index]]; + nsCOMPtr 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)) + messageArray->AppendElement(msgHdr, false); + } + } + m_folder->DownloadMessagesForOffline(messageArray, window); + return rv; +} + +nsresult nsMsgDBView::DownloadFlaggedForOffline(nsIMsgWindow *window) +{ + nsresult rv = NS_OK; + nsCOMPtr messageArray(do_CreateInstance(NS_ARRAY_CONTRACTID)); + nsCOMPtr enumerator; + rv = GetMessageEnumerator(getter_AddRefs(enumerator)); + if (NS_SUCCEEDED(rv) && enumerator) + { + bool hasMore; + + while (NS_SUCCEEDED(rv = enumerator->HasMoreElements(&hasMore)) && hasMore) + { + nsCOMPtr supports; + rv = enumerator->GetNext(getter_AddRefs(supports)); + NS_ASSERTION(NS_SUCCEEDED(rv), "nsMsgDBEnumerator broken"); + nsCOMPtr pHeader = do_QueryInterface(supports); + if (pHeader && NS_SUCCEEDED(rv)) + { + uint32_t flags; + pHeader->GetFlags(&flags); + if ((flags & nsMsgMessageFlags::Marked) && !(flags & nsMsgMessageFlags::Offline)) + messageArray->AppendElement(pHeader, false); + } + } + } + m_folder->DownloadMessagesForOffline(messageArray, 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 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 &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 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", getter_Copies(junkScoreStr)); + + // and the old origin + // + nsCString oldOriginStr; + rv = aMsgHdr->GetStringProperty("junkscoreorigin", + getter_Copies(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 folder; + nsCOMPtr 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 msgWindow(do_QueryReferent(mMsgWindowWeak)); + rv = aJunkPlugin->SetMessageClassification( + uri.get(), 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_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.get()); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +nsresult +nsMsgDBView::GetFolderFromMsgURI(const char *aMsgURI, nsIMsgFolder **aFolder) +{ + NS_IF_ADDREF(*aFolder = m_folder); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnMessageClassified(const char *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->GetLength(&numJunk); + 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->GetLength(&numJunkHdrs); + if (!numJunkHdrs) + { + NS_ERROR("no indices of marked-as-junk messages to act on"); + return NS_OK; + } + + nsCOMPtr srcFolder; + nsCOMPtr firstHdr(do_QueryElementAt(mJunkHdrs, 0)); + firstHdr->GetFolder(getter_AddRefs(srcFolder)); + + bool moveMessages, changeReadState; + nsCOMPtr 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 + + NoteStartChange(nsMsgViewNotificationCode::none, 0, 0); + rv = srcFolder->MarkMessagesRead(mJunkHdrs, msgsAreJunk); + NoteEndChange(nsMsgViewNotificationCode::none, 0, 0); + 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++) + { + nsCOMPtr junkHdr(do_QueryElementAt(mJunkHdrs, junkIndex, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + nsMsgViewIndex hdrIndex = FindHdr(junkHdr); + 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 && mCommandUpdater) + { + rv = mCommandUpdater->UpdateNextMessageAfterDelete(); + NS_ENSURE_SUCCESS(rv,rv); + } + + nsCOMPtr msgWindow(do_QueryReferent(mMsgWindowWeak)); + NoteStartChange(nsMsgViewNotificationCode::none, 0, 0); + if (targetFolder) + { + nsCOMPtr copyService = do_GetService(NS_MSGCOPYSERVICE_CONTRACTID, &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. + // + nsCOMPtr hdrsToDelete = do_CreateInstance("@mozilla.org/array;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t cnt; + rv = mJunkHdrs->GetLength(&cnt); + for (uint32_t i = 0; i < cnt; i++) + { + nsCOMPtr msgHdr = do_QueryElementAt(mJunkHdrs, i); + if (msgHdr) + { + uint32_t flags; + msgHdr->GetFlags(&flags); + if (!(flags & nsMsgMessageFlags::IMAPDeleted)) + hdrsToDelete->AppendElement(msgHdr, false); + } + } + hdrsToDelete->GetLength(&cnt); + if (cnt) + 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 imapFolder(do_QueryInterface(srcFolder)); + nsTArray imapUids; + imapUids.SetLength(numJunkHdrs); + for (uint32_t i = 0; i < numJunkHdrs; i++) + { + nsCOMPtr msgHdr = do_QueryElementAt(mJunkHdrs, i); + msgHdr->GetMessageKey(&imapUids[i]); + } + + imapFolder->StoreImapFlags(kImapMsgDeletedFlag, false, imapUids.Elements(), + imapUids.Length(), nullptr); + } + NoteEndChange(nsMsgViewNotificationCode::none, 0, 0); + + 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 server; + nsresult rv = srcFolder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr 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 rootMsgFolder; + rv = server->GetRootMsgFolder(getter_AddRefs(rootMsgFolder)); + NS_ENSURE_SUCCESS(rv,rv); + rootMsgFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox, targetFolder); + moveMessages = targetFolder != nullptr; + } + return NS_OK; + } + + nsCOMPtr 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(getter_Copies(spamFolderURI)); + NS_ENSURE_SUCCESS(rv,rv); + + NS_ASSERTION(!spamFolderURI.IsEmpty(), "spam folder URI is empty, can't move"); + if (!spamFolderURI.IsEmpty()) + { + rv = GetExistingFolder(spamFolderURI, targetFolder); + if (NS_SUCCEEDED(rv) && *targetFolder) + { + moveMessages = true; + } + else + { + // XXX ToDo: GetOrCreateFolder 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 = GetOrCreateFolder(spamFolderURI, nullptr /* aListener */); + if (NS_SUCCEEDED(rv)) + rv = GetExistingFolder(spamFolderURI, targetFolder); + NS_ASSERTION(NS_SUCCEEDED(rv) && *targetFolder, "GetOrCreateFolder 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 newFlagArray; + nsTArray newKeyArray; + nsTArray 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++; + } + nextThread = startThread; // because we're copying in reverse order + } + } + m_keys.SwapElements(newKeyArray); + m_flags.SwapElements(newFlagArray); + m_levels.SwapElements(newLevelArray); +} + +void nsMsgDBView::ReverseSort() +{ + uint32_t topIndex = GetSize(); + + nsCOMArray *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 void *pItem1, const void *pItem2, void *privateData) +{ + int32_t retVal = 0; + + IdKey** p1 = (IdKey**)pItem1; + IdKey** p2 = (IdKey**)pItem2; + viewSortInfo* sortInfo = (viewSortInfo *) privateData; + + nsIMsgDatabase *db = sortInfo->db; + + mozilla::DebugOnly rv = db->CompareCollationKeys((*p1)->dword, (*p1)->key, + (*p2)->dword, (*p2)->key, + &retVal); + NS_ASSERTION(NS_SUCCEEDED(rv), "compare failed"); + + if (retVal) + return sortInfo->ascendingSort ? retVal : -retVal; + if (sortInfo->view->m_secondarySort == nsMsgViewSortType::byId) + return (sortInfo->view->m_secondarySortOrder == nsMsgViewSortOrder::ascending && + (*p1)->id >= (*p2)->id) ? 1 : -1; + else + return sortInfo->view->SecondarySort((*p1)->id, (*p1)->folder, (*p2)->id, (*p2)->folder, sortInfo); + // here we'd use the secondary sort +} + +int +nsMsgDBView::FnSortIdKeyPtr(const void *pItem1, const void *pItem2, void *privateData) +{ + int32_t retVal = 0; + + IdKeyPtr** p1 = (IdKeyPtr**)pItem1; + IdKeyPtr** p2 = (IdKeyPtr**)pItem2; + viewSortInfo* sortInfo = (viewSortInfo *) privateData; + + nsIMsgDatabase *db = sortInfo->db; + + mozilla::DebugOnly rv = db->CompareCollationKeys((*p1)->dword, (*p1)->key, + (*p2)->dword, (*p2)->key, + &retVal); + NS_ASSERTION(NS_SUCCEEDED(rv), "compare failed"); + + if (retVal) + return sortInfo->ascendingSort ? retVal : -retVal; + + if (sortInfo->view->m_secondarySort == nsMsgViewSortType::byId) + return (sortInfo->view->m_secondarySortOrder == nsMsgViewSortOrder::ascending && + (*p1)->id >= (*p2)->id) ? 1 : -1; + else + return sortInfo->view->SecondarySort((*p1)->id, (*p1)->folder, (*p2)->id, (*p2)->folder, sortInfo); +} + +int +nsMsgDBView::FnSortIdUint32(const void *pItem1, const void *pItem2, void *privateData) +{ + IdUint32** p1 = (IdUint32**)pItem1; + IdUint32** p2 = (IdUint32**)pItem2; + viewSortInfo* sortInfo = (viewSortInfo *) privateData; + + if ((*p1)->dword > (*p2)->dword) + return (sortInfo->ascendingSort) ? 1 : -1; + else if ((*p1)->dword < (*p2)->dword) + return (sortInfo->ascendingSort) ? -1 : 1; + if (sortInfo->view->m_secondarySort == nsMsgViewSortType::byId) + return (sortInfo->view->m_secondarySortOrder == nsMsgViewSortOrder::ascending && + (*p1)->id >= (*p2)->id) ? 1 : -1; + else + return sortInfo->view->SecondarySort((*p1)->id, (*p1)->folder, (*p2)->id, (*p2)->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 modify 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; + *pMaxLen = kMaxRecipientKey; //80 - do we need a seperate k? + } + 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); + *result = !(bits & nsMsgMessageFlags::Marked); //make flagged come out on top. + break; + case nsMsgViewSortType::byUnread: + rv = msgHdr->GetIsRead(&isRead); + if (NS_SUCCEEDED(rv)) + *result = !isRead; + break; + case nsMsgViewSortType::byJunkStatus: + { + nsCString junkScoreStr; + rv = msgHdr->GetStringProperty("junkscore", getter_Copies(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 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 thread; + rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread)); + NS_ENSURE_SUCCESS(rv, rv); + thread->GetNewestMsgDate(result); + } + else + { + rv = msgHdr->GetUint32Property("dateReceived", result); // Already in seconds... + 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.get()); + if (*stringPtr) // advance past '\r' + stringPtr++; + } + m_sortColumns.AppendElement(sortColumnInfo); + } + return NS_OK; +} + +// cf. 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) +{ + // DONE + // Handle byNone (bug 901948) ala a mail/base/modules/dbViewerWrapper.js + // 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; + + // Date and ID are unique keys, so if we are sorting by them we don't + // need to keep any secondary sort keys + if (newSort.mSortType == nsMsgViewSortType::byDate || + 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, uint8_t **result, uint32_t *len, nsIMsgCustomColumnHandler* colHandler) +{ + nsresult rv = NS_ERROR_UNEXPECTED; + NS_ENSURE_ARG_POINTER(msgHdr); + NS_ENSURE_ARG_POINTER(result); + + switch (sortType) + { + case nsMsgViewSortType::bySubject: + rv = msgHdr->GetSubjectCollationKey(len, result); + break; + case nsMsgViewSortType::byLocation: + rv = GetLocationCollationKey(msgHdr, result, len); + break; + case nsMsgViewSortType::byRecipient: + { + nsString recipients; + rv = FetchRecipients(msgHdr, recipients); + if (NS_SUCCEEDED(rv)) + { + nsCOMPtr dbToUse = m_db; + if (!dbToUse) // probably search view + { + rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv,rv); + } + rv = dbToUse->CreateCollationKey(recipients, len, result); + } + } + break; + case nsMsgViewSortType::byAuthor: + { + rv = msgHdr->GetAuthorCollationKey(len, result); + nsString author; + rv = FetchAuthor(msgHdr, author); + if (NS_SUCCEEDED(rv)) + { + nsCOMPtr dbToUse = m_db; + if (!dbToUse) // probably search view + { + rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv,rv); + } + rv = dbToUse->CreateCollationKey(author, len, result); + } + } + break; + case nsMsgViewSortType::byAccount: + case nsMsgViewSortType::byTags: + { + nsString str; + nsCOMPtr dbToUse = m_db; + + if (!dbToUse) // probably 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, len, 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 dbToUse = m_db; + if (!dbToUse) // probably search view + { + rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv,rv); + } + rv = dbToUse->CreateCollationKey(strKey, len, 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 dbToUse = m_db; + if (!dbToUse) // probably search view + { + rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv,rv); + } + rv = dbToUse->CreateCollationKey(value, len, 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 = nullptr; + *len = 0; + } + 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, uint8_t **result, uint32_t *len) +{ + nsCOMPtr folder; + + nsresult rv = msgHdr->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv,rv); + nsCOMPtr dbToUse; + rv = folder->GetMsgDatabase(getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv,rv); + + nsString locationString; + rv = folder->GetPrettiestName(locationString); + NS_ENSURE_SUCCESS(rv,rv); + + return dbToUse->CreateCollationKey(locationString, len, result); +} + +nsresult nsMsgDBView::SaveSortInfo(nsMsgViewSortTypeValue sortType, nsMsgViewSortOrderValue sortOrder) +{ + if (m_viewFolder) + { + nsCOMPtr folderInfo; + nsCOMPtr 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 folderInfo; + nsCOMPtr 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::SecondarySort(nsMsgKey key1, nsISupports *supports1, + nsMsgKey key2, nsISupports *supports2, + viewSortInfo *comparisonContext) +{ + // We need to make sure that in the case of the secondary sort field also + // matching, we don't recurse. + if (comparisonContext->isSecondarySort) + return key1 > key2; + + nsCOMPtr folder1, folder2; + nsCOMPtr hdr1, hdr2; + folder1 = do_QueryInterface(supports1); + folder2 = do_QueryInterface(supports2); + 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); + IdKeyPtr EntryInfo1, EntryInfo2; + EntryInfo1.key = nullptr; + EntryInfo2.key = nullptr; + + uint16_t maxLen; + eFieldType fieldType; + nsMsgViewSortTypeValue sortType = comparisonContext->view->m_secondarySort; + nsMsgViewSortOrderValue sortOrder = comparisonContext->view->m_secondarySortOrder; + + // 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); + + const void *pValue1 = &EntryInfo1, *pValue2 = &EntryInfo2; + + int (* comparisonFun) (const void *pItem1, const void *pItem2, void *privateData) = nullptr; + int retStatus = 0; + hdr1->GetMessageKey(&EntryInfo1.id); + hdr2->GetMessageKey(&EntryInfo2.id); + + switch (fieldType) + { + case kCollationKey: + rv = GetCollationKey(hdr1, sortType, &EntryInfo1.key, &EntryInfo1.dword, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv),"failed to create collation key"); + comparisonFun = FnSortIdKeyPtr; + break; + case kU32: + if (sortType == nsMsgViewSortType::byId) + EntryInfo1.dword = EntryInfo1.id; + else + GetLongField(hdr1, sortType, &EntryInfo1.dword, colHandler); + comparisonFun = FnSortIdUint32; + break; + default: + return 0; + } + bool saveAscendingSort = comparisonContext->ascendingSort; + comparisonContext->isSecondarySort = true; + comparisonContext->ascendingSort = (sortOrder == nsMsgViewSortOrder::ascending); + if (fieldType == kCollationKey) + { + PR_FREEIF(EntryInfo2.key); + rv = GetCollationKey(hdr2, sortType, &EntryInfo2.key, &EntryInfo2.dword, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv),"failed to create collation key"); + } + else if (fieldType == kU32) + { + if (sortType == nsMsgViewSortType::byId) + EntryInfo2.dword = EntryInfo2.id; + else + GetLongField(hdr2, sortType, &EntryInfo2.dword, colHandler); + } + retStatus = (*comparisonFun)(&pValue1, &pValue2, comparisonContext); + + comparisonContext->isSecondarySort = false; + comparisonContext->ascendingSort = saveAscendingSort; + + return retStatus; +} + + +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 the 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 ptrs; + uint32_t arraySize = GetSize(); + + if (!arraySize) + return NS_OK; + + nsCOMArray *folders = GetFolders(); + + IdKey** pPtrBase = (IdKey**)PR_Malloc(arraySize * sizeof(IdKey*)); + NS_ASSERTION(pPtrBase, "out of memory, can't sort"); + if (!pPtrBase) return NS_ERROR_OUT_OF_MEMORY; + ptrs.AppendElement((void *)pPtrBase); // remember this pointer so we can free it later + + // build up the beast, so we can sort it. + uint32_t numSoFar = 0; + const uint32_t keyOffset = offsetof(IdKey, key); + // calc max possible size needed for all the rest + uint32_t maxSize = (keyOffset + maxLen) * (arraySize - numSoFar); + + const uint32_t maxBlockSize = (uint32_t) 0xf000L; + uint32_t allocSize = std::min(maxBlockSize, maxSize); + char *pTemp = (char *) PR_Malloc(allocSize); + NS_ASSERTION(pTemp, "out of memory, can't sort"); + if (!pTemp) + { + FreeAll(&ptrs); + return NS_ERROR_OUT_OF_MEMORY; + } + + ptrs.AppendElement(pTemp); // remember this pointer so we can free it later + + char *pBase = pTemp; + bool more = true; + + nsCOMPtr msgHdr; + uint8_t *keyValue = nullptr; + uint32_t longValue; + while (more && numSoFar < arraySize) + { + nsMsgKey thisKey = m_keys[numSoFar]; + if (sortType != nsMsgViewSortType::byId) + { + rv = GetMsgHdrForViewIndex(numSoFar, getter_AddRefs(msgHdr)); + NS_ASSERTION(NS_SUCCEEDED(rv) && msgHdr, "header not found"); + if (NS_FAILED(rv) || !msgHdr) + { + FreeAll(&ptrs); + return NS_ERROR_UNEXPECTED; + } + } + else + { + msgHdr = nullptr; + } + + // Could be a problem here if the ones that appear here are different than + // the ones already in the array. + uint32_t actualFieldLen = 0; + + if (fieldType == kCollationKey) + { + rv = GetCollationKey(msgHdr, sortType, &keyValue, &actualFieldLen, colHandler); + NS_ENSURE_SUCCESS(rv,rv); + + longValue = actualFieldLen; + } + else + { + if (sortType == nsMsgViewSortType::byId) + { + longValue = thisKey; + } + else + { + rv = GetLongField(msgHdr, sortType, &longValue, colHandler); + NS_ENSURE_SUCCESS(rv,rv); + } + } + + // check to see if this entry fits into the block we have allocated so far + // pTemp - pBase = the space we have used so far + // sizeof(EntryInfo) + fieldLen = space we need for this entry + // allocSize = size of the current block + if ((uint32_t)(pTemp - pBase) + (keyOffset + actualFieldLen) >= allocSize) + { + maxSize = (keyOffset + maxLen) * (arraySize - numSoFar); + allocSize = std::min(maxBlockSize, maxSize); + // make sure allocSize is big enough for the current value + allocSize = std::max(allocSize, keyOffset + actualFieldLen); + pTemp = (char *) PR_Malloc(allocSize); + NS_ASSERTION(pTemp, "out of memory, can't sort"); + if (!pTemp) + { + FreeAll(&ptrs); + return NS_ERROR_OUT_OF_MEMORY; + } + pBase = pTemp; + ptrs.AppendElement(pTemp); // remember this pointer so we can free it later + } + + // now store this entry away in the allocated memory + IdKey *info = (IdKey*)pTemp; + pPtrBase[numSoFar] = info; + info->id = thisKey; + info->bits = m_flags[numSoFar]; + info->dword = longValue; + //info->pad = 0; + info->folder = folders ? folders->ObjectAt(numSoFar) : m_folder.get(); + + memcpy(info->key, keyValue, actualFieldLen); + //In order to align memory for systems that require it, such as HP-UX + //calculate the correct value to pad the actualFieldLen value + const uint32_t align = sizeof(IdKey) - sizeof(IdUint32) - 1; + actualFieldLen = (actualFieldLen + align) & ~align; + + pTemp += keyOffset + actualFieldLen; + ++numSoFar; + PR_Free(keyValue); + } + + viewSortInfo qsPrivateData; + qsPrivateData.view = this; + qsPrivateData.isSecondarySort = false; + qsPrivateData.ascendingSort = (sortOrder == nsMsgViewSortOrder::ascending); + + nsCOMPtr dbToUse = m_db; + + if (!dbToUse) // probably search view + GetDBForViewIndex(0, getter_AddRefs(dbToUse)); + qsPrivateData.db = dbToUse; + if (dbToUse) + { + // do the sort + switch (fieldType) + { + case kCollationKey: + NS_QuickSort(pPtrBase, numSoFar, sizeof(IdKey*), FnSortIdKey, &qsPrivateData); + break; + case kU32: + NS_QuickSort(pPtrBase, numSoFar, sizeof(IdKey*), FnSortIdUint32, &qsPrivateData); + break; + default: + NS_ERROR("not supposed to get here"); + break; + } + } + + // now put the IDs into the array in proper order + for (uint32_t i = 0; i < numSoFar; 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; + + // free all the memory we allocated + FreeAll(&ptrs); + + m_sortValid = true; + //m_db->SetSortInfo(sortType, sortOrder); + + return NS_OK; +} + +void nsMsgDBView::FreeAll(nsTArray *ptrs) +{ + int32_t i; + int32_t count = (int32_t) ptrs->Length(); + if (count == 0) + return; + + for (i=(count - 1);i>=0;i--) + PR_Free((void *) ptrs->ElementAt(i)); + ptrs->Clear(); +} + +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 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 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 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); + + if (msgIndex == nsMsgViewIndex_None) // hdr is not in view, need to find by thread + { + 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 pThread; + nsCOMPtr 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 we're not allowing dummies, and we found a dummy, look again + // one past the dummy. + else if (!allowDummy && m_flags[viewIndex] & MSG_VIEW_FLAG_DUMMY) + return m_keys.IndexOf(msgKey, viewIndex + 1); + 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 msgHdr; + nsresult rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr 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 + SelectionChanged(); + 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) +{ + nsresult rv; + + nsMsgViewIndex threadIndex; + bool inThreadedMode = (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay); + + if (inThreadedMode) + { + nsCOMPtr msgHdr; + GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr)); + 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(); + 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(); + SelectionChanged(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::GetThreadContainingMsgHdr(nsIMsgDBHdr *msgHdr, nsIMsgThread **pThread) +{ + 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 msgHdr; + nsCOMPtr pThread; + nsresult rv = GetThreadContainingIndex(index, getter_AddRefs(pThread)); + NS_ENSURE_SUCCESS(rv, rv); + if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) + { + if (flags & nsMsgMessageFlags::Read) + m_levels.AppendElement(0); // keep top level hdr in thread, even though read. + rv = ListUnreadIdsInThread(pThread, index, &numExpanded); + } + else + rv = ListIdsInThread(pThread, index, &numExpanded); + + if (numExpanded > 0) + { + m_flags[index] = flags; + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + } + NoteStartChange(index + 1, numExpanded, nsMsgViewNotificationCode::insertOrDelete); + NoteEndChange(index + 1, numExpanded, nsMsgViewNotificationCode::insertOrDelete); + if (pNumExpanded != nullptr) + *pNumExpanded = numExpanded; + return rv; +} + +nsresult nsMsgDBView::CollapseAll() +{ + 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); + } + SelectionChanged(); + 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); + + int32_t numRemoved = -rowDelta; // don't count first header in thread + 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; + } + NoteStartChange(index + 1, rowDelta, nsMsgViewNotificationCode::insertOrDelete); + // start at first id after thread. + RemoveRows(index + 1, numRemoved); + if (pNumCollapsed != nullptr) + *pNumCollapsed = numRemoved; + NoteEndChange(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 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; + IdKeyPtr EntryInfo1, EntryInfo2; + EntryInfo1.key = nullptr; + EntryInfo2.key = nullptr; + + 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); + + const void *pValue1 = &EntryInfo1, *pValue2 = &EntryInfo2; + + int retStatus = 0; + msgHdr->GetMessageKey(&EntryInfo1.id); + msgHdr->GetFolder(&EntryInfo1.folder); + EntryInfo1.folder->Release(); + + viewSortInfo comparisonContext; + comparisonContext.view = this; + comparisonContext.isSecondarySort = false; + comparisonContext.ascendingSort = (m_sortOrder == nsMsgViewSortOrder::ascending); + nsCOMPtr hdrDB; + EntryInfo1.folder->GetMsgDatabase(getter_AddRefs(hdrDB)); + comparisonContext.db = hdrDB.get(); + switch (fieldType) + { + case kCollationKey: + rv = GetCollationKey(msgHdr, m_sortType, &EntryInfo1.key, &EntryInfo1.dword, 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 tryHdr; + nsCOMPtr db; + // ### this should get the db from the folder... + GetDBForViewIndex(tryIndex, getter_AddRefs(db)); + if (db) + rv = 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) + { + PR_FREEIF(EntryInfo2.key); + rv = GetCollationKey(tryHdr, m_sortType, &EntryInfo2.key, &EntryInfo2.dword, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv),"failed to create collation key"); + retStatus = FnSortIdKeyPtr(&pValue1, &pValue2, &comparisonContext); + } + else if (fieldType == kU32) + { + if (m_sortType == nsMsgViewSortType::byId) + EntryInfo2.dword = EntryInfo2.id; + else + GetLongField(tryHdr, m_sortType, &EntryInfo2.dword, colHandler); + retStatus = FnSortIdUint32(&pValue1, &pValue2, &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++; + } + } + + PR_Free(EntryInfo1.key); + PR_Free(EntryInfo2.key); + return highIndex; +} + +nsMsgViewIndex nsMsgDBView::GetInsertIndexHelper(nsIMsgDBHdr *msgHdr, nsTArray &keys, + nsCOMArray *folders, + nsMsgViewSortOrderValue sortOrder, nsMsgViewSortTypeValue sortType) +{ + nsMsgViewIndex highIndex = keys.Length(); + nsMsgViewIndex lowIndex = 0; + IdKeyPtr EntryInfo1, EntryInfo2; + EntryInfo1.key = nullptr; + EntryInfo2.key = nullptr; + + 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); + + const void *pValue1 = &EntryInfo1, *pValue2 = &EntryInfo2; + + int (* comparisonFun) (const void *pItem1, const void *pItem2, void *privateData) = nullptr; + int retStatus = 0; + msgHdr->GetMessageKey(&EntryInfo1.id); + msgHdr->GetFolder(&EntryInfo1.folder); + EntryInfo1.folder->Release(); + + viewSortInfo comparisonContext; + comparisonContext.view = this; + comparisonContext.isSecondarySort = false; + comparisonContext.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, &EntryInfo1.dword, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv),"failed to create collation key"); + comparisonFun = FnSortIdKeyPtr; + break; + case kU32: + if (sortType == nsMsgViewSortType::byId) + EntryInfo1.dword = EntryInfo1.id; + else + GetLongField(msgHdr, sortType, &EntryInfo1.dword, colHandler); + comparisonFun = FnSortIdUint32; + 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 tryHdr; + EntryInfo2.folder->GetMessageHeader(EntryInfo2.id, getter_AddRefs(tryHdr)); + if (!tryHdr) + break; + if (fieldType == kCollationKey) + { + PR_FREEIF(EntryInfo2.key); + rv = GetCollationKey(tryHdr, sortType, &EntryInfo2.key, &EntryInfo2.dword, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv),"failed to create collation key"); + } + else if (fieldType == kU32) + { + if (sortType == nsMsgViewSortType::byId) { + EntryInfo2.dword = EntryInfo2.id; + } + else { + GetLongField(tryHdr, sortType, &EntryInfo2.dword, colHandler); + } + } + retStatus = (*comparisonFun)(&pValue1, &pValue2, &comparisonContext); + if (retStatus == 0) + { + highIndex = tryIndex; + break; + } + + if (retStatus < 0) + { + highIndex = tryIndex; + } + else + { + lowIndex = tryIndex + 1; + } + } + + PR_Free(EntryInfo1.key); + PR_Free(EntryInfo2.key); + 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 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); + // ### 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*/) +{ + return true; // default is to want all threads. +} + +nsMsgViewIndex nsMsgDBView::FindParentInThread(nsMsgKey parentKey, nsMsgViewIndex startOfThreadViewIndex) +{ + nsCOMPtr 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) +{ + nsresult rv = NS_OK; + nsCOMPtr msgEnumerator; + threadHdr->EnumerateMessages(parentKey, getter_AddRefs(msgEnumerator)); + uint32_t numChildren; + (void) threadHdr->GetNumChildren(&numChildren); + NS_ASSERTION(numChildren, "Empty thread in view/db"); + if (!numChildren) + return NS_OK; // bogus, but harmless. + + numChildren--; // account for the existing thread root + + // skip the first one. + bool hasMore; + nsCOMPtr supports; + nsCOMPtr msgHdr; + while (NS_SUCCEEDED(rv) && NS_SUCCEEDED(rv = msgEnumerator->HasMoreElements(&hasMore)) && hasMore) + { + rv = msgEnumerator->GetNext(getter_AddRefs(supports)); + if (NS_SUCCEEDED(rv) && supports) + { + if (*pNumListed == numChildren) + { + NS_NOTREACHED("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); + rv = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + break; + } + + msgHdr = do_QueryInterface(supports); + 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); + } + } + return rv; // we don't want to return the rv from the enumerator when it reaches the end, do we? +} + +bool nsMsgDBView::InsertEmptyRows(nsMsgViewIndex viewIndex, int32_t numRows) +{ + return 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 *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; + } + + if (m_keys.InsertElementsAt(aIndex, aNumRows, aKey) && + m_flags.InsertElementsAt(aIndex, aNumRows, aFlags) && + m_levels.InsertElementsAt(aIndex, aNumRows, aLevel)) + return NS_OK; + + return NS_ERROR_UNEXPECTED; +} + +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 *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; + + numChildren--; // account for the existing thread root + if (!InsertEmptyRows(viewIndex, numChildren)) + return NS_ERROR_OUT_OF_MEMORY; + + // ### 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 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) + { + NS_NOTREACHED("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) + // ### 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 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 infinte 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; +} + +// ### 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; + IdKeyPtr EntryInfo1, EntryInfo2; + EntryInfo1.key = nullptr; + EntryInfo2.key = nullptr; + + 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); + + const void *pValue1 = &EntryInfo1, *pValue2 = &EntryInfo2; + + int retStatus = 0; + msgHdr->GetMessageKey(&EntryInfo1.id); + msgHdr->GetFolder(&EntryInfo1.folder); + EntryInfo1.folder->Release(); + + viewSortInfo comparisonContext; + comparisonContext.view = this; + comparisonContext.isSecondarySort = false; + comparisonContext.ascendingSort = (m_sortOrder == nsMsgViewSortOrder::ascending); + nsCOMPtr hdrDB; + EntryInfo1.folder->GetMsgDatabase(getter_AddRefs(hdrDB)); + comparisonContext.db = hdrDB.get(); + switch (fieldType) + { + case kCollationKey: + rv = GetCollationKey(msgHdr, m_sortType, &EntryInfo1.key, &EntryInfo1.dword, 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 tryHdr; + nsCOMPtr db; + // ### this should get the db from the folder... + GetDBForViewIndex(tryIndex, getter_AddRefs(db)); + if (db) + rv = db->GetMsgHdrForKey(EntryInfo2.id, getter_AddRefs(tryHdr)); + if (!tryHdr) + break; + if (tryHdr == msgHdr) + { + highIndex = tryIndex; + break; + } + if (fieldType == kCollationKey) + { + PR_FREEIF(EntryInfo2.key); + rv = GetCollationKey(tryHdr, m_sortType, &EntryInfo2.key, &EntryInfo2.dword, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv),"failed to create collation key"); + retStatus = FnSortIdKeyPtr(&pValue1, &pValue2, &comparisonContext); + } + else if (fieldType == kU32) + { + if (m_sortType == nsMsgViewSortType::byId) + EntryInfo2.dword = EntryInfo2.id; + else + GetLongField(tryHdr, m_sortType, &EntryInfo2.dword, colHandler); + retStatus = FnSortIdUint32(&pValue1, &pValue2, &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 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; + } + PR_Free(EntryInfo1.key); + PR_Free(EntryInfo2.key); + return msgHdr == resultHdr ? highIndex : nsMsgViewIndex_None; +} + +#ifdef DEBUG_David_Bienvenu + +void nsMsgDBView::InitEntryInfoForIndex(nsMsgViewIndex i, IdKeyPtr &EntryInfo) +{ + EntryInfo.key = nullptr; + + 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 msgHdr; + GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr)); + + msgHdr->GetMessageKey(&EntryInfo.id); + msgHdr->GetFolder(&EntryInfo.folder); + EntryInfo.folder->Release(); + + nsCOMPtr hdrDB; + EntryInfo.folder->GetMsgDatabase(getter_AddRefs(hdrDB)); + switch (fieldType) + { + case kCollationKey: + PR_FREEIF(EntryInfo.key); + rv = GetCollationKey(msgHdr, m_sortType, &EntryInfo.key, &EntryInfo.dword, 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() +{ + IdKeyPtr EntryInfo1, EntryInfo2; + nsCOMPtr 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 + 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 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 = FnSortIdKeyPtr(&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 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); + + // ### 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. + if (mTreeSelection && mCommandUpdater) + { + bool isMsgSelected = false; + mTreeSelection->IsSelected(deletedIndex, &isMsgSelected); + if (isMsgSelected) + mCommandUpdater->UpdateNextMessageAfterDelete(); + } + + RemoveByIndex(deletedIndex); + } + + 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, 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); + + 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 && !mSuppressChangeNotification) + { + switch (changeType) + { + case nsMsgViewNotificationCode::changed: + mTree->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() + mTree->RowCountChanged(firstLineChanged, numChanged); + mRemovingRow = false; + MOZ_FALLTHROUGH; + case nsMsgViewNotificationCode::all: + ClearHdrCache(); + break; + } + } + return NS_OK; +} + +void nsMsgDBView::NoteStartChange(nsMsgViewIndex firstlineChanged, int32_t numChanged, + nsMsgViewNotificationCodeValue changeType) +{ +} +void nsMsgDBView::NoteEndChange(nsMsgViewIndex firstlineChanged, int32_t numChanged, + nsMsgViewNotificationCodeValue changeType) +{ + // send the notification now. + NoteChange(firstlineChanged, numChanged, changeType); +} + +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(); + m_sortValid = false; // invalidate the sort so sorting will do something + } + m_viewFlags = aViewFlags; + + if (m_viewFolder) + { + nsCOMPtr db; + nsCOMPtr 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 &idsMarkedRead, bool bRead) +{ + nsCOMPtr 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 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 &idsMarkedRead, bool bRead) +{ + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + idsMarkedRead.SetCapacity(numChildren); + for (int32_t childIndex = 0; childIndex < (int32_t) numChildren ; childIndex++) + { + nsCOMPtr msgHdr; + threadHdr->GetChildHdrAt(childIndex, getter_AddRefs(msgHdr)); + NS_ASSERTION(msgHdr, "msgHdr is null"); + if (!msgHdr) + continue; + + bool isRead; + + nsMsgKey hdrMsgId; + msgHdr->GetMessageKey(&hdrMsgId); + nsCOMPtr 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; + } + + 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: + *pResultIndex = (startIndex != nsMsgViewIndex_None && startIndex > 0) ? startIndex - 1 : 0; + *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 thru - is this motion ever used? + MOZ_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 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 (startIndex == nsMsgViewIndex_None) + 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 threadHdr; + nsCOMPtr 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, 1); + + return NavigateFromPos(nsMsgNavigationType::nextUnreadMessage, startIndex, pResultKey, pResultIndex, pThreadIndex, true); + case nsMsgNavigationType::toggleThreadKilled: + { + bool resultKilled; + nsMsgViewIndexArray selection; + GetSelectedIndices(selection); + ToggleIgnored(selection.Elements(), selection.Length(), &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; + GetSelectedIndices(selection); + ToggleMessageKilled(selection.Elements(), selection.Length(), + &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: + { + nsCString folderUri, msgUri; + nsCString viewFolderUri; + nsCOMPtr curFolder = m_viewFolder ? m_viewFolder : m_folder; + if (curFolder) + curFolder->GetURI(viewFolderUri); + int32_t relPos = (motion == nsMsgNavigationType::forward) + ? 1 : (m_currentlyDisplayedMsgKey != nsMsgKey_None) ? -1 : 0; + int32_t curPos; + nsresult rv; + nsCOMPtr messenger (do_QueryReferent(mMessengerWeak, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + rv = messenger->GetFolderUriAtNavigatePos(relPos, folderUri); + NS_ENSURE_SUCCESS(rv, rv); + // Empty viewFolderUri means we're in a search results view. + if (viewFolderUri.IsEmpty() || folderUri.Equals(viewFolderUri)) + { + nsCOMPtr msgHdr; + rv = messenger->GetMsgUriAtNavigatePos(relPos, msgUri); + NS_ENSURE_SUCCESS(rv, rv); + messenger->MsgHdrFromURI(msgUri, getter_AddRefs(msgHdr)); + if (msgHdr) + { + messenger->GetNavigatePos(&curPos); + curPos += relPos; + *pResultIndex = FindHdr(msgHdr); + messenger->SetNavigatePos(curPos); + msgHdr->GetMessageKey(pResultKey); + return NS_OK; + } + } + *pResultIndex = nsMsgViewIndex_None; + *pResultKey = nsMsgKey_None; + break; + + } + default: + NS_ERROR("unsupported motion"); + break; + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBView::NavigateStatus(nsMsgNavigationTypeValue motion, bool *_retval) +{ + NS_ENSURE_ARG_POINTER(_retval); + + bool enable = false; + nsresult rv = NS_ERROR_FAILURE; + nsMsgKey resultKey = nsMsgKey_None; + int32_t index = nsMsgKey_None; + nsMsgViewIndex resultIndex = nsMsgViewIndex_None; + if (mTreeSelection) + (void) mTreeSelection->GetCurrentIndex(&index); + else + index = FindViewIndex(m_currentlyDisplayedMsgKey); + nsCOMPtr messenger (do_QueryReferent(mMessengerWeak)); + // warning - we no longer validate index up front because fe passes in -1 for no + // selection, so if you use index, be sure to validate it before using it + // as an array index. + switch (motion) + { + case nsMsgNavigationType::firstMessage: + case nsMsgNavigationType::lastMessage: + if (GetSize() > 0) + enable = true; + break; + case nsMsgNavigationType::nextMessage: + if (IsValidIndex(index) && uint32_t(index) < GetSize() - 1) + enable = true; + break; + case nsMsgNavigationType::previousMessage: + if (IsValidIndex(index) && index != 0 && GetSize() > 1) + enable = true; + break; + case nsMsgNavigationType::firstFlagged: + rv = FindFirstFlagged(&resultIndex); + enable = (NS_SUCCEEDED(rv) && resultIndex != nsMsgViewIndex_None); + break; + case nsMsgNavigationType::nextFlagged: + rv = FindNextFlagged(index + 1, &resultIndex); + enable = (NS_SUCCEEDED(rv) && resultIndex != nsMsgViewIndex_None); + break; + case nsMsgNavigationType::previousFlagged: + if (IsValidIndex(index) && index != 0) + rv = FindPrevFlagged(index, &resultIndex); + enable = (NS_SUCCEEDED(rv) && resultIndex != nsMsgViewIndex_None); + break; + case nsMsgNavigationType::firstNew: + rv = FindFirstNew(&resultIndex); + enable = (NS_SUCCEEDED(rv) && resultIndex != nsMsgViewIndex_None); + break; + case nsMsgNavigationType::readMore: + enable = true; // for now, always true. + break; + case nsMsgNavigationType::nextFolder: + case nsMsgNavigationType::nextUnreadThread: + case nsMsgNavigationType::nextUnreadMessage: + case nsMsgNavigationType::toggleThreadKilled: + enable = true; // always enabled + break; + case nsMsgNavigationType::previousUnreadMessage: + if (IsValidIndex(index)) + { + nsMsgKey threadId; + rv = FindPrevUnread(m_keys[index], &resultKey, &threadId); + enable = (resultKey != nsMsgKey_None); + } + break; + case nsMsgNavigationType::forward: + case nsMsgNavigationType::back: + { + uint32_t curPos; + uint32_t historyCount; + + if (messenger) + { + messenger->GetNavigateHistory(&curPos, &historyCount, nullptr); + int32_t desiredPos = (int32_t) curPos; + if (motion == nsMsgNavigationType::forward) + desiredPos++; + else + desiredPos--; //? operator code didn't work for me + enable = (desiredPos >= 0 && desiredPos < (int32_t) historyCount / 2); + } + } + break; + + default: + NS_ERROR("unexpected"); + break; + } + + *_retval = enable; + 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(nsMsgViewIndex * indices, int32_t numIndices, nsMsgViewIndex *resultIndex, bool *resultToggleState) +{ + nsCOMPtr thread; + + // Ignored state is toggled based on the first selected thread + nsMsgViewIndex threadIndex = GetThreadFromMsgIndex(indices[0], getter_AddRefs(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; + while (numIndices) + { + numIndices--; + if (indices[numIndices] < threadIndex) + { + threadIndex = GetThreadFromMsgIndex(indices[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(nsMsgViewIndex * indices, int32_t numIndices, nsMsgViewIndex *resultIndex, bool *resultToggleState) +{ + NS_ENSURE_ARG_POINTER(resultToggleState); + + nsCOMPtr header; + // Ignored state is toggled based on the first selected message + nsresult rv = GetMsgHdrForViewIndex(indices[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; + while (numIndices) + { + numIndices--; + if (indices[numIndices] < msgIndex) + { + msgIndex = indices[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) +{ + nsMsgKey msgKey = GetAt(index); + nsMsgViewIndex threadIndex; + + if (threadHdr == nullptr) + return nsMsgViewIndex_None; + + nsresult rv = GetThreadContainingIndex(index, threadHdr); + NS_ENSURE_SUCCESS(rv,nsMsgViewIndex_None); + + if (*threadHdr == nullptr) + return nsMsgViewIndex_None; + + nsMsgKey threadKey; + (*threadHdr)->GetThreadKey(&threadKey); + if (msgKey !=threadKey) + threadIndex = GetIndexOfFirstDisplayedKeyInThread(*threadHdr); + else + threadIndex = index; + return threadIndex; +} + +nsresult nsMsgDBView::ToggleWatched( nsMsgViewIndex* indices, int32_t numIndices) +{ + nsCOMPtr thread; + + // Watched state is toggled based on the first selected thread + nsMsgViewIndex threadIndex = GetThreadFromMsgIndex(indices[0], getter_AddRefs(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; + while (numIndices) + { + numIndices--; + if (indices[numIndices] < threadIndex) + { + threadIndex = GetThreadFromMsgIndex(indices[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 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 thread; + nsresult rv; + rv = GetThreadContainingMsgHdr(header, getter_AddRefs(thread)); + if (NS_FAILED(rv)) + return NS_OK; // So we didn't mark threads read + + 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 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; + + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + return m_db->MarkThreadWatched(thread, m_keys[index], watched, this); +} + +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; + GetSelectedIndices(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 folder; + GetMsgFolder(getter_AddRefs(folder)); + nsCOMPtr 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 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 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 (!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) + { + 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) + mTree->BeginUpdateBatch(); + for (uint32_t i = 0; i < numIndices; i++) + NoteChange(mIndicesToNoteChange[i], -1, nsMsgViewNotificationCode::insertOrDelete); + if (numIndices > 1) + mTree->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(nsMsgViewIndex * indices, int32_t numIndices) +{ + nsCOMPtr localFolder = do_QueryInterface(m_folder); + if (localFolder) + return true; + + for (nsMsgViewIndex index = 0; index < (nsMsgViewIndex) numIndices; index++) + { + // For cross-folder saved searches, we need to check if any message + // is in a local folder. + if (!m_folder) + { + nsCOMPtr folder; + GetFolderForViewIndex(indices[index], getter_AddRefs(folder)); + nsCOMPtr localFolder = do_QueryInterface(folder); + if (localFolder) + return true; + } + + uint32_t flags = m_flags[indices[index]]; + if ((flags & nsMsgMessageFlags::Offline)) + return true; + } + return false; +} + +bool nsMsgDBView::NonDummyMsgSelected(nsMsgViewIndex * indices, int32_t numIndices) +{ + bool includeCollapsedMsgs = OperateOnMsgsInCollapsedThreads(); + + for (nsMsgViewIndex index = 0; index < (nsMsgViewIndex) numIndices; index++) + { + uint32_t flags = m_flags[indices[index]]; + // 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 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* 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); + } + return NS_OK; +} + +nsresult nsMsgDBView::GetImapDeleteModel(nsIMsgFolder *folder) +{ + nsresult rv = NS_OK; + nsCOMPtr server; + if (folder) //for the search view + folder->GetServer(getter_AddRefs(server)); + else if (m_folder) + m_folder->GetServer(getter_AddRefs(server)); + nsCOMPtr 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, + nsIDOMDataTransfer *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, + nsIDOMDataTransfer *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 preservedSelection; + nsresult rv = SaveAndClearSelection(nullptr, preservedSelection); + NS_ENSURE_SUCCESS(rv,rv); + + // now, restore our desired selection + AutoTArray 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(); + + if (!newMsgDBView) + return NS_ERROR_OUT_OF_MEMORY; + + 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 = 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; + aNewMsgDBView->mDateFormatter = mDateFormatter; + 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; + aNewMsgDBView->m_levels = m_levels; + aNewMsgDBView->m_keys = m_keys; + + aNewMsgDBView->m_customColumnHandlerIDs = m_customColumnHandlerIDs; + aNewMsgDBView->m_customColumnHandlers.AppendObjects(m_customColumnHandlers); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSearchSession(nsIMsgSearchSession* *aSession) +{ + NS_ASSERTION(false, "should be overriden by child class"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDBView::SetSearchSession(nsIMsgSearchSession *aSession) +{ + NS_ASSERTION(false, "should be overriden 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, nsDateFormatSelector& _format ) +{ + // read + int32_t nFormatSetting( 0 ); + nsresult result = _prefBranch->GetIntPref( _prefLocalName, &nFormatSetting ); + if ( NS_SUCCEEDED( result ) ) + { + // translate + nsDateFormatSelector res( nFormatSetting ); + // transfer if valid + if ( ( res >= kDateFormatNone ) && ( res <= kDateFormatWeekday ) ) + _format = res; + } +} + +nsresult nsMsgDBView::InitDisplayFormats() +{ + m_dateFormatDefault = kDateFormatShort; + m_dateFormatThisWeek = kDateFormatShort; + m_dateFormatToday = kDateFormatNone; + + nsresult rv = NS_OK; + nsCOMPtr prefs = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv,rv); + nsCOMPtr 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); +} + + +NS_IMPL_ISUPPORTS(nsMsgDBView::nsMsgViewHdrEnumerator, nsISimpleEnumerator) + +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 clonedView; + view->CloneDBView(nullptr, nullptr, nullptr, getter_AddRefs(clonedView)); + m_view = static_cast(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(nsISupports **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 nextHdr; + + nsresult rv = m_view->GetMsgHdrForViewIndex(m_curHdrIndex++, getter_AddRefs(nextHdr)); + NS_IF_ADDREF(*aItem = nextHdr); + 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(nsISimpleEnumerator **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 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 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 server; + folder->GetServer(getter_AddRefs(server)); + nsAutoCString type; + if (server) + server->GetType(type); + if (!(MsgLowerCaseEqualsLiteral(type, "nntp") || MsgLowerCaseEqualsLiteral(type, "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; +} -- cgit v1.2.3