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