diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/base/src/nsMsgFolderCompactor.cpp | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mailnews/base/src/nsMsgFolderCompactor.cpp')
-rw-r--r-- | comm/mailnews/base/src/nsMsgFolderCompactor.cpp | 1391 |
1 files changed, 1391 insertions, 0 deletions
diff --git a/comm/mailnews/base/src/nsMsgFolderCompactor.cpp b/comm/mailnews/base/src/nsMsgFolderCompactor.cpp new file mode 100644 index 0000000000..c9740bedb8 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgFolderCompactor.cpp @@ -0,0 +1,1391 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" // precompiled header... +#include "nsCOMPtr.h" +#include "nsIMsgFolder.h" +#include "nsIFile.h" +#include "nsNetUtil.h" +#include "nsIMsgHdr.h" +#include "nsIChannel.h" +#include "nsIStreamListener.h" +#include "nsIMsgMessageService.h" +#include "nsMsgUtils.h" +#include "nsISeekableStream.h" +#include "nsIDBFolderInfo.h" +#include "nsIPrompt.h" +#include "nsIMsgLocalMailFolder.h" +#include "nsIMsgImapMailFolder.h" +#include "nsMailHeaders.h" +#include "nsMsgLocalFolderHdrs.h" +#include "nsIMsgDatabase.h" +#include "nsMsgMessageFlags.h" +#include "nsMsgFolderFlags.h" +#include "nsIMsgStatusFeedback.h" +#include "nsIMsgFolderNotificationService.h" +#include "nsMsgFolderCompactor.h" +#include "nsIOutputStream.h" +#include "nsIInputStream.h" +#include "nsPrintfCString.h" +#include "nsIStringBundle.h" +#include "nsICopyMessageStreamListener.h" +#include "nsIMsgWindow.h" +#include "nsIMsgPluggableStore.h" +#include "mozilla/Buffer.h" +#include "HeaderReader.h" +#include "LineReader.h" +#include "mozilla/Components.h" + +static nsresult GetBaseStringBundle(nsIStringBundle** aBundle) { + NS_ENSURE_ARG_POINTER(aBundle); + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsIStringBundle> bundle; + return bundleService->CreateBundle( + "chrome://messenger/locale/messenger.properties", aBundle); +} + +#define COMPACTOR_READ_BUFF_SIZE 16384 + +/** + * nsFolderCompactState is a helper class for nsFolderCompactor, which + * handles compacting the mbox for a single local folder. + * + * This class also patches X-Mozilla-* headers where required. Usually + * these headers are edited in-place without changing the overall size, + * but sometimes there's not enough room. So as compaction involves + * rewriting the whole file anyway, we take the opportunity to make some + * more space and correct those headers. + * + * NOTE (for future cleanups): + * + * This base class calls nsIMsgMessageService.copyMessages() to iterate + * through messages, passing itself in as a listener. Callbacks from + * both nsICopyMessageStreamListener and nsIStreamListener are invoked. + * + * nsOfflineStoreCompactState uses a different mechanism - see separate + * notes below. + * + * The way the service invokes the listener callbacks is pretty quirky + * and probably needs a good sorting out, but for now I'll just document what + * I've observed here: + * + * - The service calls OnStartRequest() at the start of the first message. + * - StartMessage() is called at the start of subsequent messages. + * - EndCopy() is called at the end of every message except the last one, + * where OnStopRequest() is invoked instead. + * - OnDataAvailable() is called to pass the message body of each message + * (in multiple calls if the message is big enough). + * - EndCopy() doesn't ever seem to be passed a failing error code from + * what I can see, and its own return code is ignored by upstream code. + */ +class nsFolderCompactState : public nsIStreamListener, + public nsICopyMessageStreamListener, + public nsIUrlListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSICOPYMESSAGESTREAMLISTENER + NS_DECL_NSIURLLISTENER + + nsFolderCompactState(void); + + nsresult Compact(nsIMsgFolder* folder, + std::function<void(nsresult, uint64_t)> completionFn, + nsIMsgWindow* msgWindow); + + protected: + virtual ~nsFolderCompactState(void); + + virtual nsresult InitDB(nsIMsgDatabase* db); + virtual nsresult StartCompacting(); + virtual nsresult FinishCompact(); + void CloseOutputStream(); + void CleanupTempFilesAfterError(); + nsresult FlushBuffer(); + + nsresult Init(nsIMsgFolder* aFolder, const char* aBaseMsgUri, + nsIMsgDatabase* aDb, nsIFile* aPath, nsIMsgWindow* aMsgWindow); + nsresult BuildMessageURI(const char* baseURI, nsMsgKey key, nsCString& uri); + nsresult ShowStatusMsg(const nsString& aMsg); + nsresult ReleaseFolderLock(); + void ShowCompactingStatusMsg(); + + nsCString m_baseMessageUri; // base message uri + nsCString m_messageUri; // current message uri being copy + nsCOMPtr<nsIMsgFolder> m_folder; // current folder being compact + nsCOMPtr<nsIMsgDatabase> m_db; // new database for the compact folder + nsCOMPtr<nsIFile> m_file; // new mailbox for the compact folder + nsCOMPtr<nsIOutputStream> m_fileStream; // output file stream for writing + // All message keys that need to be copied over. + nsTArray<nsMsgKey> m_keys; + + // Sum of the sizes of the messages, accumulated as we visit each msg. + uint64_t m_totalMsgSize{0}; + // Number of bytes that can be expunged while compacting. + uint64_t m_totalExpungedBytes{0}; + // Index of the current copied message key in key array. + uint32_t m_curIndex{0}; + // Offset in mailbox of new message. + uint64_t m_startOfNewMsg{0}; + mozilla::Buffer<char> m_buffer{COMPACTOR_READ_BUFF_SIZE}; + uint32_t m_bufferCount{0}; + + // We'll use this if we need to output any EOLs - we try to preserve the + // convention found in the input data. + nsCString m_eolSeq{MSG_LINEBREAK}; + + // The status of the copying operation. + nsresult m_status{NS_OK}; + nsCOMPtr<nsIMsgMessageService> m_messageService; // message service for + // copying + nsCOMPtr<nsIMsgWindow> m_window; + nsCOMPtr<nsIMsgDBHdr> m_curSrcHdr; + // Flag set when we're waiting for local folder to complete parsing. + bool m_parsingFolder; + // Flag to indicate we're starting a new message, and that no data has + // been written for it yet. + bool m_startOfMsg; + // Function which will be run when the folder compaction completes. + // Takes a result code and the number of bytes which were expunged. + std::function<void(nsresult, uint64_t)> m_completionFn; + bool m_alreadyWarnedDiskSpace{false}; +}; + +NS_IMPL_ISUPPORTS(nsFolderCompactState, nsIRequestObserver, nsIStreamListener, + nsICopyMessageStreamListener, nsIUrlListener) + +nsFolderCompactState::nsFolderCompactState() { + m_parsingFolder = false; + m_startOfMsg = true; +} + +nsFolderCompactState::~nsFolderCompactState() { + CloseOutputStream(); + if (NS_FAILED(m_status)) { + CleanupTempFilesAfterError(); + // if for some reason we failed remove the temp folder and database + } +} + +void nsFolderCompactState::CloseOutputStream() { + if (m_fileStream) { + m_fileStream->Close(); + m_fileStream = nullptr; + } +} + +void nsFolderCompactState::CleanupTempFilesAfterError() { + CloseOutputStream(); + if (m_db) m_db->ForceClosed(); + nsCOMPtr<nsIFile> summaryFile; + GetSummaryFileLocation(m_file, getter_AddRefs(summaryFile)); + m_file->Remove(false); + summaryFile->Remove(false); +} + +nsresult nsFolderCompactState::BuildMessageURI(const char* baseURI, + nsMsgKey key, nsCString& uri) { + uri.Append(baseURI); + uri.Append('#'); + uri.AppendInt(key); + + return NS_OK; +} + +nsresult nsFolderCompactState::InitDB(nsIMsgDatabase* db) { + nsCOMPtr<nsIMsgDatabase> mailDBFactory; + nsresult rv = db->ListAllKeys(m_keys); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = msgDBService->OpenMailDBFromFile(m_file, m_folder, true, false, + getter_AddRefs(m_db)); + + if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE || + rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) + // if it's out of date then reopen with upgrade. + return msgDBService->OpenMailDBFromFile(m_file, m_folder, true, true, + getter_AddRefs(m_db)); + return rv; +} + +nsresult nsFolderCompactState::Compact( + nsIMsgFolder* folder, std::function<void(nsresult, uint64_t)> completionFn, + nsIMsgWindow* msgWindow) { + NS_ENSURE_ARG_POINTER(folder); + m_completionFn = completionFn; + m_window = msgWindow; + nsresult rv; + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIFile> path; + nsCString baseMessageURI; + + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(folder, &rv); + if (NS_SUCCEEDED(rv) && localFolder) { + rv = localFolder->GetDatabaseWOReparse(getter_AddRefs(db)); + if (NS_FAILED(rv) || !db) { + if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING || + rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) { + m_folder = folder; // will be used to compact + m_parsingFolder = true; + rv = localFolder->ParseFolder(m_window, this); + } + return rv; + } else { + bool valid; + rv = db->GetSummaryValid(&valid); + if (!valid) // we are probably parsing the folder because we selected it. + { + folder->NotifyCompactCompleted(); + if (m_completionFn) { + m_completionFn(NS_OK, m_totalExpungedBytes); + } + return NS_OK; + } + } + } else { + rv = folder->GetMsgDatabase(getter_AddRefs(db)); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = folder->GetFilePath(getter_AddRefs(path)); + NS_ENSURE_SUCCESS(rv, rv); + + do { + bool exists = false; + rv = path->Exists(&exists); + if (!exists) { + // No need to compact if the local file does not exist. + // Can happen e.g. on IMAP when the folder is not marked for offline use. + break; + } + + int64_t expunged = 0; + folder->GetExpungedBytes(&expunged); + if (expunged == 0) { + // No need to compact if nothing would be expunged. + break; + } + + int64_t diskSize; + rv = folder->GetSizeOnDisk(&diskSize); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t diskFree; + rv = path->GetDiskSpaceAvailable(&diskFree); + if (NS_FAILED(rv)) { + // If GetDiskSpaceAvailable() failed, better bail out fast. + if (rv != NS_ERROR_NOT_IMPLEMENTED) return rv; + // Some platforms do not have GetDiskSpaceAvailable implemented. + // In that case skip the preventive free space analysis and let it + // fail in compact later if space actually wasn't available. + } else { + // Let's try to not even start compact if there is really low free space. + // It may still fail later as we do not know how big exactly the folder DB + // will end up being. The DB already doesn't contain references to + // messages that are already deleted. So theoretically it shouldn't shrink + // with compact. But in practice, the automatic shrinking of the DB may + // still have not yet happened. So we cap the final size at 1KB per + // message. + db->Commit(nsMsgDBCommitType::kCompressCommit); + + int64_t dbSize; + rv = db->GetDatabaseSize(&dbSize); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t totalMsgs; + rv = folder->GetTotalMessages(false, &totalMsgs); + NS_ENSURE_SUCCESS(rv, rv); + int64_t expectedDBSize = + std::min<int64_t>(dbSize, ((int64_t)totalMsgs) * 1024); + if (diskFree < diskSize - expunged + expectedDBSize) { + if (!m_alreadyWarnedDiskSpace) { + folder->ThrowAlertMsg("compactFolderInsufficientSpace", m_window); + m_alreadyWarnedDiskSpace = true; + } + break; + } + } + + rv = folder->GetBaseMessageURI(baseMessageURI); + NS_ENSURE_SUCCESS(rv, rv); + + rv = Init(folder, baseMessageURI.get(), db, path, m_window); + NS_ENSURE_SUCCESS(rv, rv); + + bool isLocked = true; + m_folder->GetLocked(&isLocked); + if (isLocked) { + CleanupTempFilesAfterError(); + m_folder->ThrowAlertMsg("compactFolderDeniedLock", m_window); + break; + } + + // If we got here start the real compacting. + nsCOMPtr<nsISupports> supports; + QueryInterface(NS_GET_IID(nsISupports), getter_AddRefs(supports)); + m_folder->AcquireSemaphore(supports); + m_totalExpungedBytes += expunged; + return StartCompacting(); + + } while (false); // block for easy skipping the compaction using 'break' + + // Skipped folder, for whatever reason. + folder->NotifyCompactCompleted(); + if (m_completionFn) { + m_completionFn(NS_OK, m_totalExpungedBytes); + } + return NS_OK; +} + +nsresult nsFolderCompactState::ShowStatusMsg(const nsString& aMsg) { + if (!m_window || aMsg.IsEmpty()) return NS_OK; + + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + nsresult rv = m_window->GetStatusFeedback(getter_AddRefs(statusFeedback)); + if (NS_FAILED(rv) || !statusFeedback) return NS_OK; + + // Try to prepend account name to the message. + nsString statusMessage; + do { + nsCOMPtr<nsIMsgIncomingServer> server; + rv = m_folder->GetServer(getter_AddRefs(server)); + if (NS_FAILED(rv)) break; + nsAutoString accountName; + rv = server->GetPrettyName(accountName); + if (NS_FAILED(rv)) break; + nsCOMPtr<nsIStringBundle> bundle; + rv = GetBaseStringBundle(getter_AddRefs(bundle)); + if (NS_FAILED(rv)) break; + AutoTArray<nsString, 2> params = {accountName, aMsg}; + rv = bundle->FormatStringFromName("statusMessage", params, statusMessage); + } while (false); + + // If fetching any of the needed info failed, just show the original message. + if (NS_FAILED(rv)) statusMessage.Assign(aMsg); + return statusFeedback->SetStatusString(statusMessage); +} + +nsresult nsFolderCompactState::Init(nsIMsgFolder* folder, + const char* baseMsgUri, nsIMsgDatabase* db, + nsIFile* path, nsIMsgWindow* aMsgWindow) { + nsresult rv; + + m_folder = folder; + m_baseMessageUri = baseMsgUri; + m_file = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + m_file->InitWithFile(path); + + m_file->SetNativeLeafName("nstmp"_ns); + // Make sure we are not crunching existing nstmp file. + rv = m_file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600); + NS_ENSURE_SUCCESS(rv, rv); + + m_window = aMsgWindow; + m_totalMsgSize = 0; + rv = InitDB(db); + if (NS_FAILED(rv)) { + CleanupTempFilesAfterError(); + return rv; + } + + m_curIndex = 0; + + rv = MsgNewBufferedFileOutputStream(getter_AddRefs(m_fileStream), m_file, -1, + 00600); + if (NS_FAILED(rv)) + m_folder->ThrowAlertMsg("compactFolderWriteFailed", m_window); + else + rv = GetMessageServiceFromURI(nsDependentCString(baseMsgUri), + getter_AddRefs(m_messageService)); + if (NS_FAILED(rv)) { + m_status = rv; + } + return rv; +} + +void nsFolderCompactState::ShowCompactingStatusMsg() { + nsString statusString; + nsresult rv = m_folder->GetStringWithFolderNameFromBundle("compactingFolder", + statusString); + if (!statusString.IsEmpty() && NS_SUCCEEDED(rv)) ShowStatusMsg(statusString); +} + +NS_IMETHODIMP nsFolderCompactState::OnStartRunningUrl(nsIURI* url) { + return NS_OK; +} + +// If we had to kick off a folder parse, this will be called when it +// completes. +NS_IMETHODIMP nsFolderCompactState::OnStopRunningUrl(nsIURI* url, + nsresult status) { + if (m_parsingFolder) { + m_parsingFolder = false; + if (NS_SUCCEEDED(status)) { + // Folder reparse succeeded. Start compacting it. + status = Compact(m_folder, m_completionFn, m_window); + if (NS_SUCCEEDED(status)) { + return NS_OK; + } + } + } + + // This is from bug 249754. The aim is to close the DB file to avoid + // running out of filehandles when large numbers of folders are compacted. + // But it seems like filehandle management would be better off being + // handled by the DB class itself (it might be already, but it's hard to + // tell)... + m_folder->SetMsgDatabase(nullptr); + + if (m_completionFn) { + m_completionFn(status, m_totalExpungedBytes); + } + return NS_OK; +} + +nsresult nsFolderCompactState::StartCompacting() { + nsresult rv = NS_OK; + // Notify that compaction is beginning. We do this even if there are no + // messages to be copied because the summary database still gets blown away + // which is still pretty interesting. (And we like consistency.) + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) { + notifier->NotifyFolderCompactStart(m_folder); + } + + // TODO: test whether sorting the messages (m_keys) by messageOffset + // would improve performance on large files (less seeks). + // The m_keys array is in the order as stored in DB and on IMAP or News + // the messages stored on the mbox file are not necessarily in the same order. + if (m_keys.Length() > 0) { + nsCOMPtr<nsIURI> notUsed; + ShowCompactingStatusMsg(); + NS_ADDREF_THIS(); + rv = m_messageService->CopyMessages(m_keys, m_folder, this, false, nullptr, + m_window, getter_AddRefs(notUsed)); + } else { // no messages to copy with + FinishCompact(); + } + return rv; +} + +nsresult nsFolderCompactState::FinishCompact() { + NS_ENSURE_TRUE(m_folder, NS_ERROR_NOT_INITIALIZED); + NS_ENSURE_TRUE(m_file, NS_ERROR_NOT_INITIALIZED); + + // All okay time to finish up the compact process + nsCOMPtr<nsIFile> path; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + + // get leaf name and database name of the folder + nsresult rv = m_folder->GetFilePath(getter_AddRefs(path)); + nsCOMPtr<nsIFile> folderPath = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = folderPath->InitWithFile(path); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> oldSummaryFile; + rv = GetSummaryFileLocation(folderPath, getter_AddRefs(oldSummaryFile)); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString dbName; + oldSummaryFile->GetNativeLeafName(dbName); + nsAutoCString folderName; + path->GetNativeLeafName(folderName); + + // close down the temp file stream; preparing for deleting the old folder + // and its database; then rename the temp folder and database + if (m_fileStream) { + m_fileStream->Flush(); + m_fileStream->Close(); + m_fileStream = nullptr; + } + + // make sure the new database is valid. + // Close it so we can rename the .msf file. + if (m_db) { + m_db->ForceClosed(); + m_db = nullptr; + } + + nsCOMPtr<nsIFile> newSummaryFile; + rv = GetSummaryFileLocation(m_file, getter_AddRefs(newSummaryFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDBFolderInfo> transferInfo; + m_folder->GetDBTransferInfo(getter_AddRefs(transferInfo)); + + // close down database of the original folder + m_folder->ForceDBClosed(); + + nsCOMPtr<nsIFile> cloneFile; + int64_t fileSize = 0; + rv = m_file->Clone(getter_AddRefs(cloneFile)); + if (NS_SUCCEEDED(rv)) rv = cloneFile->GetFileSize(&fileSize); + bool tempFileRightSize = ((uint64_t)fileSize == m_totalMsgSize); + NS_WARNING_ASSERTION(tempFileRightSize, + "temp file not of expected size in compact"); + + bool folderRenameSucceeded = false; + bool msfRenameSucceeded = false; + if (NS_SUCCEEDED(rv) && tempFileRightSize) { + // First we're going to try and move the old summary file out the way. + // We don't delete it yet, as we want to keep the files in sync. + nsCOMPtr<nsIFile> tempSummaryFile; + rv = oldSummaryFile->Clone(getter_AddRefs(tempSummaryFile)); + if (NS_SUCCEEDED(rv)) + rv = tempSummaryFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + + nsAutoCString tempSummaryFileName; + if (NS_SUCCEEDED(rv)) + rv = tempSummaryFile->GetNativeLeafName(tempSummaryFileName); + + if (NS_SUCCEEDED(rv)) + rv = oldSummaryFile->MoveToNative((nsIFile*)nullptr, tempSummaryFileName); + + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "error moving compacted folder's db out of the way"); + if (NS_SUCCEEDED(rv)) { + // Now we've successfully moved the summary file out the way, try moving + // the newly compacted message file over the old one. + rv = m_file->MoveToNative((nsIFile*)nullptr, folderName); + folderRenameSucceeded = NS_SUCCEEDED(rv); + NS_WARNING_ASSERTION(folderRenameSucceeded, + "error renaming compacted folder"); + if (folderRenameSucceeded) { + // That worked, so land the new summary file in the right place. + nsCOMPtr<nsIFile> renamedCompactedSummaryFile; + newSummaryFile->Clone(getter_AddRefs(renamedCompactedSummaryFile)); + if (renamedCompactedSummaryFile) { + rv = renamedCompactedSummaryFile->MoveToNative((nsIFile*)nullptr, + dbName); + msfRenameSucceeded = NS_SUCCEEDED(rv); + } + NS_WARNING_ASSERTION(msfRenameSucceeded, + "error renaming compacted folder's db"); + } + + if (!msfRenameSucceeded) { + // Do our best to put the summary file back to where it was + rv = tempSummaryFile->MoveToNative((nsIFile*)nullptr, dbName); + if (NS_SUCCEEDED(rv)) { + // Flagging that a renamed db no longer exists. + tempSummaryFile = nullptr; + } else { + NS_WARNING("error restoring uncompacted folder's db"); + } + } + } + // We don't want any temporarily renamed summary file to lie around + if (tempSummaryFile) tempSummaryFile->Remove(false); + } + + NS_WARNING_ASSERTION(msfRenameSucceeded, "compact failed"); + nsresult rvReleaseFolderLock = ReleaseFolderLock(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvReleaseFolderLock), + "folder lock not released successfully"); + rv = NS_FAILED(rv) ? rv : rvReleaseFolderLock; + + // Cleanup of nstmp-named compacted files if failure + if (!folderRenameSucceeded) { + // remove the abandoned compacted version with the wrong name + m_file->Remove(false); + } + if (!msfRenameSucceeded) { + // remove the abandoned compacted summary file + newSummaryFile->Remove(false); + } + + if (msfRenameSucceeded) { + // Transfer local db information from transferInfo + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = msgDBService->OpenFolderDB(m_folder, true, getter_AddRefs(m_db)); + NS_ENSURE_TRUE(m_db, NS_FAILED(rv) ? rv : NS_ERROR_FAILURE); + // These errors are expected. + rv = (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING || + rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) + ? NS_OK + : rv; + m_db->SetSummaryValid(true); + if (transferInfo) m_folder->SetDBTransferInfo(transferInfo); + + // since we're transferring info from the old db, we need to reset the + // expunged bytes + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (dbFolderInfo) dbFolderInfo->SetExpungedBytes(0); + } + if (m_db) m_db->Close(true); + m_db = nullptr; + + // Notify that compaction of the folder is completed. + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) { + notifier->NotifyFolderCompactFinish(m_folder); + } + + m_folder->NotifyCompactCompleted(); + if (m_completionFn) { + m_completionFn(rv, m_totalExpungedBytes); + } + + return NS_OK; +} + +nsresult nsFolderCompactState::ReleaseFolderLock() { + nsresult result = NS_OK; + if (!m_folder) return result; + bool haveSemaphore; + nsCOMPtr<nsISupports> supports; + QueryInterface(NS_GET_IID(nsISupports), getter_AddRefs(supports)); + result = m_folder->TestSemaphore(supports, &haveSemaphore); + if (NS_SUCCEEDED(result) && haveSemaphore) + result = m_folder->ReleaseSemaphore(supports); + return result; +} + +NS_IMETHODIMP +nsFolderCompactState::OnStartRequest(nsIRequest* request) { + return StartMessage(); +} + +NS_IMETHODIMP +nsFolderCompactState::OnStopRequest(nsIRequest* request, nsresult status) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + if (NS_FAILED(status)) { + // Set m_status to status so the destructor can remove the temp folder and + // database. + m_status = status; + CleanupTempFilesAfterError(); + m_folder->NotifyCompactCompleted(); + ReleaseFolderLock(); + m_folder->ThrowAlertMsg("compactFolderWriteFailed", m_window); + } else { + // XXX TODO: Error checking and handling missing here. + EndCopy(nullptr, status); + if (m_curIndex >= m_keys.Length()) { + msgHdr = nullptr; + // no more to copy finish it up + FinishCompact(); + } else { + // in case we're not getting an error, we still need to pretend we did get + // an error, because the compact did not successfully complete. + m_folder->NotifyCompactCompleted(); + CleanupTempFilesAfterError(); + ReleaseFolderLock(); + } + } + NS_RELEASE_THIS(); // kill self + return status; +} + +// Handle the message data. +// (NOTE: nsOfflineStoreCompactState overrides this) +NS_IMETHODIMP +nsFolderCompactState::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) { + MOZ_ASSERT(m_fileStream); + MOZ_ASSERT(inStr); + + nsresult rv = NS_OK; + + // TODO: This block should be moved in to the callback that indicates the + // start of a new message, but it's complicated because of the derived + // nsOfflineStoreCompactState and also the odd message copy listener + // orderings. Leaving it here for now, but it's ripe for tidying up in + // future. + if (m_startOfMsg) { + m_messageUri.Truncate(); // clear the previous message uri + if (NS_SUCCEEDED(BuildMessageURI(m_baseMessageUri.get(), m_keys[m_curIndex], + m_messageUri))) { + rv = m_messageService->MessageURIToMsgHdr(m_messageUri, + getter_AddRefs(m_curSrcHdr)); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + while (count > 0) { + uint32_t maxReadCount = + std::min((uint32_t)m_buffer.Length() - m_bufferCount, count); + uint32_t readCount; + rv = inStr->Read(m_buffer.Elements() + m_bufferCount, maxReadCount, + &readCount); + NS_ENSURE_SUCCESS(rv, rv); + + count -= readCount; + m_bufferCount += readCount; + if (m_bufferCount == m_buffer.Length()) { + rv = FlushBuffer(); + NS_ENSURE_SUCCESS(rv, rv); + } + } + if (m_bufferCount > 0) { + rv = FlushBuffer(); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +// Helper to write data to an outputstream, until complete or error. +static nsresult WriteSpan(nsIOutputStream* writeable, + mozilla::Span<const char> data) { + while (!data.IsEmpty()) { + uint32_t n; + nsresult rv = writeable->Write(data.Elements(), data.Length(), &n); + NS_ENSURE_SUCCESS(rv, rv); + data = data.Last(data.Length() - n); + } + return NS_OK; +} + +// Flush contents of m_buffer to the output file. +// (NOTE: not used by nsOfflineStoreCompactState) +// More complicated than it should be because we need to fiddle with +// some of the X-Mozilla-* headers on the fly. +nsresult nsFolderCompactState::FlushBuffer() { + MOZ_ASSERT(m_fileStream); + nsresult rv; + auto buf = m_buffer.AsSpan().First(m_bufferCount); + if (!m_startOfMsg) { + // We only do header twiddling for the first chunk. So from now on we + // just copy data verbatim. + rv = WriteSpan(m_fileStream, buf); + NS_ENSURE_SUCCESS(rv, rv); + m_bufferCount = 0; + return NS_OK; + } + + // This is the first chunk of a new message. We'll update the + // X-Mozilla-(Status|Status2|Keys) headers as we go. + m_startOfMsg = false; + + // Sniff the data to see if we can spot any CRs. + // If so, we'll use CRLFs instead of platform-native EOLs. + auto sniffChunk = buf.First(std::min<size_t>(buf.Length(), 512)); + auto cr = std::find(sniffChunk.cbegin(), sniffChunk.cend(), '\r'); + if (cr != sniffChunk.cend()) { + m_eolSeq.Assign("\r\n"_ns); + } + + // Add a "From " line if missing. + // NOTE: Ultimately we should never see "From " lines in this data - it's an + // mbox detail the message streaming should filter out. But for now we'll + // handle it optionally. + nsAutoCString fromLine; + auto l = FirstLine(buf); + if (l.Length() > 5 && + nsDependentCSubstring(l.Elements(), 5).EqualsLiteral("From ")) { + fromLine = nsDependentCSubstring(l); + buf = buf.From(l.Length()); + } else { + fromLine = "From "_ns + m_eolSeq; + } + rv = WriteSpan(m_fileStream, fromLine); + NS_ENSURE_SUCCESS(rv, rv); + + // Read as many headers as we can. We might not have the complete header + // block our in buffer, but that's OK - the X-Mozilla-* ones should be + // right at the start). + nsTArray<HeaderReader::Hdr> headers; + HeaderReader rdr; + auto leftover = rdr.Parse(buf, [&](auto const& hdr) -> bool { + auto const& name = hdr.Name(buf); + if (!name.EqualsLiteral(HEADER_X_MOZILLA_STATUS) && + !name.EqualsLiteral(HEADER_X_MOZILLA_STATUS2) && + !name.EqualsLiteral(HEADER_X_MOZILLA_KEYWORDS)) { + headers.AppendElement(hdr); + } + return true; + }); + + // Write out X-Mozilla-* headers first - we'll create these from scratch. + uint32_t msgFlags = 0; + nsAutoCString keywords; + if (m_curSrcHdr) { + m_curSrcHdr->GetFlags(&msgFlags); + m_curSrcHdr->GetStringProperty("keywords", keywords); + // growKeywords is set if msgStore didn't have enough room to edit + // X-Mozilla-* headers in situ. We'll rewrite all those headers + // regardless but we still want to clear it. + uint32_t grow; + m_curSrcHdr->GetUint32Property("growKeywords", &grow); + if (grow) { + m_curSrcHdr->SetUint32Property("growKeywords", 0); + } + } + + auto out = + nsPrintfCString(HEADER_X_MOZILLA_STATUS ": %4.4x", msgFlags & 0xFFFF); + out.Append(m_eolSeq); + rv = WriteSpan(m_fileStream, out); + NS_ENSURE_SUCCESS(rv, rv); + + out = nsPrintfCString(HEADER_X_MOZILLA_STATUS2 ": %8.8x", + msgFlags & 0xFFFF0000); + out.Append(m_eolSeq); + rv = WriteSpan(m_fileStream, out); + NS_ENSURE_SUCCESS(rv, rv); + + // Try to leave room for future in-place keyword edits. + while (keywords.Length() < X_MOZILLA_KEYWORDS_BLANK_LEN) { + keywords.Append(' '); + } + out = nsPrintfCString(HEADER_X_MOZILLA_KEYWORDS ": %s", keywords.get()); + out.Append(m_eolSeq); + rv = WriteSpan(m_fileStream, out); + NS_ENSURE_SUCCESS(rv, rv); + + // Write out the rest of the headers. + for (auto const& hdr : headers) { + rv = WriteSpan(m_fileStream, buf.Subspan(hdr.pos, hdr.len)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // The header parser consumes the blank line, If we've completed parsing + // we need to output it now. + // If we haven't parsed all the headers yet then the blank line will be + // safely copied verbatim as part of the remaining data. + if (rdr.IsComplete()) { + rv = WriteSpan(m_fileStream, m_eolSeq); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Write out everything else in the buffer verbatim. + if (leftover.Length() > 0) { + rv = WriteSpan(m_fileStream, leftover); + NS_ENSURE_SUCCESS(rv, rv); + } + m_bufferCount = 0; + return NS_OK; +} + +/** + * nsOfflineStoreCompactState is a helper class for nsFolderCompactor which + * handles compacting the mbox for a single offline IMAP folder. + * + * nsOfflineStoreCompactState does *not* do any special X-Mozilla-* header + * handling, unlike the base class. + * + * NOTE (for future cleanups): + * This class uses a different mechanism to iterate through messages. It uses + * nsIMsgMessageService.streamMessage() to stream each message in turn, + * passing itself in as an nsIStreamListener. The nsICopyMessageStreamListener + * callbacks implemented in the base class are _not_ used here. + * For each message, the standard OnStartRequest(), OnDataAvailable()..., + * OnStopRequest() sequence is seen. + * Nothing too fancy, but it's not always clear where code from the base class + * is being used and when it is not, so it can be complicated to pick through. + * + */ +class nsOfflineStoreCompactState : public nsFolderCompactState { + public: + nsOfflineStoreCompactState(void); + virtual ~nsOfflineStoreCompactState(void); + NS_IMETHOD OnStopRequest(nsIRequest* request, nsresult status) override; + NS_IMETHODIMP OnDataAvailable(nsIRequest* request, nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) override; + + protected: + nsresult CopyNextMessage(bool& done); + virtual nsresult InitDB(nsIMsgDatabase* db) override; + virtual nsresult StartCompacting() override; + virtual nsresult FinishCompact() override; + + char m_dataBuffer[COMPACTOR_READ_BUFF_SIZE + 1]; // temp data buffer for + // copying message + uint32_t m_offlineMsgSize; +}; + +nsOfflineStoreCompactState::nsOfflineStoreCompactState() + : m_offlineMsgSize(0) {} + +nsOfflineStoreCompactState::~nsOfflineStoreCompactState() {} + +nsresult nsOfflineStoreCompactState::InitDB(nsIMsgDatabase* db) { + // Start with the list of messages we have offline as the possible + // message to keep when compacting the offline store. + db->ListAllOfflineMsgs(m_keys); + m_db = db; + return NS_OK; +} + +/** + * This will copy one message to the offline store, but if it fails to + * copy the next message, it will keep trying messages until it finds one + * it can copy, or it runs out of messages. + */ +nsresult nsOfflineStoreCompactState::CopyNextMessage(bool& done) { + while (m_curIndex < m_keys.Length()) { + // Filter out msgs that have the "pendingRemoval" attribute set. + nsCOMPtr<nsIMsgDBHdr> hdr; + nsCString pendingRemoval; + nsresult rv = + m_db->GetMsgHdrForKey(m_keys[m_curIndex], getter_AddRefs(hdr)); + NS_ENSURE_SUCCESS(rv, rv); + hdr->GetStringProperty("pendingRemoval", pendingRemoval); + if (!pendingRemoval.IsEmpty()) { + m_curIndex++; + // Turn off offline flag for message, since after the compact is + // completed; we won't have the message in the offline store. + uint32_t resultFlags; + hdr->AndFlags(~nsMsgMessageFlags::Offline, &resultFlags); + // We need to clear this in case the user changes the offline retention + // settings. + hdr->SetStringProperty("pendingRemoval", ""_ns); + continue; + } + m_messageUri.Truncate(); // clear the previous message uri + rv = BuildMessageURI(m_baseMessageUri.get(), m_keys[m_curIndex], + m_messageUri); + NS_ENSURE_SUCCESS(rv, rv); + m_startOfMsg = true; + nsCOMPtr<nsISupports> thisSupports; + QueryInterface(NS_GET_IID(nsISupports), getter_AddRefs(thisSupports)); + nsCOMPtr<nsIURI> dummyNull; + rv = m_messageService->StreamMessage(m_messageUri, thisSupports, m_window, + nullptr, false, EmptyCString(), true, + getter_AddRefs(dummyNull)); + // if copy fails, we clear the offline flag on the source message. + if (NS_FAILED(rv)) { + nsCOMPtr<nsIMsgDBHdr> hdr; + m_messageService->MessageURIToMsgHdr(m_messageUri, getter_AddRefs(hdr)); + if (hdr) { + uint32_t resultFlags; + hdr->AndFlags(~nsMsgMessageFlags::Offline, &resultFlags); + } + m_curIndex++; + continue; + } else + break; + } + done = m_curIndex >= m_keys.Length(); + // In theory, we might be able to stream the next message, so + // return NS_OK, not rv. + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineStoreCompactState::OnStopRequest(nsIRequest* request, + nsresult status) { + nsresult rv = status; + nsCOMPtr<nsIURI> uri; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + nsCOMPtr<nsIChannel> channel; + bool done = false; + + // The NS_MSG_ERROR_MSG_NOT_OFFLINE error should allow us to continue, so we + // check for it specifically and don't terminate the compaction. + if (NS_FAILED(rv) && rv != NS_MSG_ERROR_MSG_NOT_OFFLINE) goto done; + + // We know the request is an nsIChannel we can get a URI from, but this is + // probably bad form. See Bug 1528662. + channel = do_QueryInterface(request, &rv); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "error QI nsIRequest to nsIChannel failed"); + if (NS_FAILED(rv)) goto done; + rv = channel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) goto done; + rv = m_messageService->MessageURIToMsgHdr(m_messageUri, + getter_AddRefs(msgHdr)); + if (NS_FAILED(rv)) goto done; + + // This is however an unexpected condition, so let's print a warning. + if (rv == NS_MSG_ERROR_MSG_NOT_OFFLINE) { + nsAutoCString spec; + uri->GetSpec(spec); + nsPrintfCString msg("Message expectedly not available offline: %s", + spec.get()); + NS_WARNING(msg.get()); + } + + if (msgHdr) { + if (NS_SUCCEEDED(status)) { + msgHdr->SetMessageOffset(m_startOfNewMsg); + nsCString storeToken = nsPrintfCString("%" PRIu64, m_startOfNewMsg); + msgHdr->SetStringProperty("storeToken", storeToken); + msgHdr->SetOfflineMessageSize(m_offlineMsgSize); + } else { + uint32_t resultFlags; + msgHdr->AndFlags(~nsMsgMessageFlags::Offline, &resultFlags); + } + } + + if (m_window) { + m_window->GetStatusFeedback(getter_AddRefs(statusFeedback)); + if (statusFeedback) + statusFeedback->ShowProgress(100 * m_curIndex / m_keys.Length()); + } + // advance to next message + m_curIndex++; + rv = CopyNextMessage(done); + if (done) { + m_db->Commit(nsMsgDBCommitType::kCompressCommit); + msgHdr = nullptr; + // no more to copy finish it up + ReleaseFolderLock(); + FinishCompact(); + NS_RELEASE_THIS(); // kill self + } + +done: + if (NS_FAILED(rv)) { + m_status = rv; // set the status to rv so the destructor can remove the + // temp folder and database + ReleaseFolderLock(); + NS_RELEASE_THIS(); // kill self + + if (m_completionFn) { + m_completionFn(m_status, m_totalExpungedBytes); + } + return rv; + } + return rv; +} + +nsresult nsOfflineStoreCompactState::FinishCompact() { + // All okay time to finish up the compact process + nsCOMPtr<nsIFile> path; + uint32_t flags; + + // get leaf name and database name of the folder + m_folder->GetFlags(&flags); + nsresult rv = m_folder->GetFilePath(getter_AddRefs(path)); + + nsCString leafName; + path->GetNativeLeafName(leafName); + + if (m_fileStream) { + // close down the temp file stream; preparing for deleting the old folder + // and its database; then rename the temp folder and database + m_fileStream->Flush(); + m_fileStream->Close(); + m_fileStream = nullptr; + } + + // make sure the new database is valid + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (dbFolderInfo) dbFolderInfo->SetExpungedBytes(0); + // this forces the m_folder to update mExpungedBytes from the db folder info. + int64_t expungedBytes; + m_folder->GetExpungedBytes(&expungedBytes); + m_folder->UpdateSummaryTotals(true); + m_db->SetSummaryValid(true); + + // remove the old folder + path->Remove(false); + + // rename the copied folder to be the original folder + m_file->MoveToNative((nsIFile*)nullptr, leafName); + + ShowStatusMsg(EmptyString()); + m_folder->NotifyCompactCompleted(); + if (m_completionFn) { + m_completionFn(NS_OK, m_totalExpungedBytes); + } + return rv; +} + +NS_IMETHODIMP +nsFolderCompactState::Init(nsICopyMessageListener* destination) { + return NS_OK; +} + +NS_IMETHODIMP +nsFolderCompactState::StartMessage() { + nsresult rv = NS_ERROR_FAILURE; + NS_ASSERTION(m_fileStream, "Fatal, null m_fileStream..."); + if (m_fileStream) { + nsCOMPtr<nsISeekableStream> seekableStream = + do_QueryInterface(m_fileStream, &rv); + NS_ENSURE_SUCCESS(rv, rv); + // this will force an internal flush, but not a sync. Tell should really do + // an internal flush, but it doesn't, and I'm afraid to change that + // nsIFileStream.cpp code anymore. + seekableStream->Seek(nsISeekableStream::NS_SEEK_CUR, 0); + // record the new message key for the message + int64_t curStreamPos; + seekableStream->Tell(&curStreamPos); + m_startOfNewMsg = curStreamPos; + rv = NS_OK; + } + return rv; +} + +NS_IMETHODIMP +nsFolderCompactState::EndMessage(nsMsgKey key) { return NS_OK; } + +// XXX TODO: This function is sadly lacking all status checking, it always +// returns NS_OK and moves onto the next message. +NS_IMETHODIMP +nsFolderCompactState::EndCopy(nsIURI* uri, nsresult status) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCOMPtr<nsIMsgDBHdr> newMsgHdr; + + if (m_curIndex >= m_keys.Length()) { + NS_ASSERTION(false, "m_curIndex out of bounds"); + return NS_OK; + } + + // Take note of the end offset of the message (without the trailing blank + // line). + nsCOMPtr<nsITellableStream> tellable(do_QueryInterface(m_fileStream)); + MOZ_ASSERT(tellable); + int64_t endOfMsg; + nsresult rv = tellable->Tell(&endOfMsg); + NS_ENSURE_SUCCESS(rv, rv); + + /* Messages need to have trailing blank lines */ + rv = WriteSpan(m_fileStream, m_eolSeq); + NS_ENSURE_SUCCESS(rv, rv); + + /* + * Done with the current message; copying the existing message header + * to the new database. + */ + if (m_curSrcHdr) { + nsMsgKey key; + m_curSrcHdr->GetMessageKey(&key); + m_db->CopyHdrFromExistingHdr(key, m_curSrcHdr, true, + getter_AddRefs(newMsgHdr)); + } + m_curSrcHdr = nullptr; + if (newMsgHdr) { + nsCString storeToken = nsPrintfCString("%" PRIu64, m_startOfNewMsg); + newMsgHdr->SetStringProperty("storeToken", storeToken); + newMsgHdr->SetMessageOffset(m_startOfNewMsg); + uint64_t msgSize = endOfMsg - m_startOfNewMsg; + newMsgHdr->SetMessageSize(msgSize); + + m_totalMsgSize += msgSize + m_eolSeq.Length(); + } + + // m_db->Commit(nsMsgDBCommitType::kLargeCommit); // no sense committing + // until the end + // advance to next message + m_curIndex++; + m_startOfMsg = true; + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + if (m_window) { + m_window->GetStatusFeedback(getter_AddRefs(statusFeedback)); + if (statusFeedback) + statusFeedback->ShowProgress(100 * m_curIndex / m_keys.Length()); + } + return NS_OK; +} + +nsresult nsOfflineStoreCompactState::StartCompacting() { + nsresult rv = NS_OK; + if (m_keys.Length() > 0 && m_curIndex == 0) { + NS_ADDREF_THIS(); // we own ourselves, until we're done, anyway. + ShowCompactingStatusMsg(); + bool done = false; + rv = CopyNextMessage(done); + if (!done) return rv; + } + ReleaseFolderLock(); + FinishCompact(); + return rv; +} + +NS_IMETHODIMP +nsOfflineStoreCompactState::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStr, + uint64_t sourceOffset, + uint32_t count) { + if (!m_fileStream || !inStr) return NS_ERROR_FAILURE; + + nsresult rv = NS_OK; + + if (m_startOfMsg) { + m_offlineMsgSize = 0; + m_messageUri.Truncate(); // clear the previous message uri + if (NS_SUCCEEDED(BuildMessageURI(m_baseMessageUri.get(), m_keys[m_curIndex], + m_messageUri))) { + rv = m_messageService->MessageURIToMsgHdr(m_messageUri, + getter_AddRefs(m_curSrcHdr)); + NS_ENSURE_SUCCESS(rv, rv); + } + } + uint32_t maxReadCount, readCount, writeCount; + uint32_t bytesWritten; + + while (NS_SUCCEEDED(rv) && (int32_t)count > 0) { + maxReadCount = + count > sizeof(m_dataBuffer) - 1 ? sizeof(m_dataBuffer) - 1 : count; + writeCount = 0; + rv = inStr->Read(m_dataBuffer, maxReadCount, &readCount); + + if (NS_SUCCEEDED(rv)) { + if (m_startOfMsg) { + m_startOfMsg = false; + // check if there's an envelope header; if not, write one. + if (strncmp(m_dataBuffer, "From ", 5)) { + m_fileStream->Write("From " CRLF, 7, &bytesWritten); + m_offlineMsgSize += bytesWritten; + } + } + m_fileStream->Write(m_dataBuffer, readCount, &bytesWritten); + m_offlineMsgSize += bytesWritten; + writeCount += bytesWritten; + count -= readCount; + if (writeCount != readCount) { + m_folder->ThrowAlertMsg("compactFolderWriteFailed", m_window); + return NS_MSG_ERROR_WRITING_MAIL_FOLDER; + } + } + } + return rv; +} + +////////////////////////////////////////////////////////////////////////////// +// nsMsgFolderCompactor implementation +////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS(nsMsgFolderCompactor, nsIMsgFolderCompactor) + +nsMsgFolderCompactor::nsMsgFolderCompactor() {} + +nsMsgFolderCompactor::~nsMsgFolderCompactor() {} + +NS_IMETHODIMP nsMsgFolderCompactor::CompactFolders( + const nsTArray<RefPtr<nsIMsgFolder>>& folders, nsIUrlListener* listener, + nsIMsgWindow* window) { + MOZ_ASSERT(mQueue.IsEmpty()); + mWindow = window; + mListener = listener; + mTotalBytesGained = 0; + mQueue = folders.Clone(); + mQueue.Reverse(); + + // Can't guarantee that anyone will keep us in scope until we're done, so... + MOZ_ASSERT(!mKungFuDeathGrip); + mKungFuDeathGrip = this; + + // nsIMsgFolderCompactor idl states this isn't called... + // but maybe it should be? + // if (mListener) { + // mListener->OnStartRunningUrl(nullptr); + // } + + NextFolder(); + + return NS_OK; +} + +void nsMsgFolderCompactor::NextFolder() { + while (!mQueue.IsEmpty()) { + // Should only ever have one compactor running. + MOZ_ASSERT(mCompactor == nullptr); + + nsCOMPtr<nsIMsgFolder> folder = mQueue.PopLastElement(); + + // Sanity check - should we be compacting this folder? + nsCOMPtr<nsIMsgPluggableStore> msgStore; + nsresult rv = folder->GetMsgStore(getter_AddRefs(msgStore)); + if (NS_FAILED(rv)) { + NS_WARNING("Skipping folder with no msgStore"); + continue; + } + bool storeSupportsCompaction; + msgStore->GetSupportsCompaction(&storeSupportsCompaction); + if (!storeSupportsCompaction) { + NS_WARNING("Trying to compact a non-mbox folder"); + continue; // just skip it. + } + + nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(folder)); + if (imapFolder) { + uint32_t flags; + folder->GetFlags(&flags); + if (flags & nsMsgFolderFlags::Offline) { + mCompactor = new nsOfflineStoreCompactState(); + } + } else { + mCompactor = new nsFolderCompactState(); + } + if (!mCompactor) { + NS_WARNING("skipping compact of non-offline folder"); + continue; + } + nsCString uri; + folder->GetURI(uri); + + // Callback for when a folder compaction completes. + auto completionFn = [self = RefPtr<nsMsgFolderCompactor>(this), + compactState = mCompactor](nsresult status, + uint64_t expungedBytes) { + if (NS_SUCCEEDED(status)) { + self->mTotalBytesGained += expungedBytes; + } else { + // Failed. We want to keep going with the next folder, but make sure + // we return a failing code upon overall completion. + self->mOverallStatus = status; + NS_WARNING("folder compact failed."); + } + + // Release our lock on the compactor - it's done. + self->mCompactor = nullptr; + self->NextFolder(); + }; + + rv = mCompactor->Compact(folder, completionFn, mWindow); + if (NS_SUCCEEDED(rv)) { + // Now wait for the compactor to let us know it's finished, + // via the completion callback fn. + return; + } + mOverallStatus = rv; + mCompactor = nullptr; + NS_WARNING("folder compact failed - skipping folder"); + } + + // Done. No more folders to compact. + + if (mListener) { + // If there were multiple failures, this will communicate only the + // last one, but that's OK. Main thing is to indicate that _something_ + // went wrong. + mListener->OnStopRunningUrl(nullptr, mOverallStatus); + } + ShowDoneStatus(); + + // We're not needed any more. + mKungFuDeathGrip = nullptr; + return; +} + +void nsMsgFolderCompactor::ShowDoneStatus() { + if (!mWindow) { + return; + } + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS_VOID(rv); + nsAutoString expungedAmount; + FormatFileSize(mTotalBytesGained, true, expungedAmount); + AutoTArray<nsString, 1> params = {expungedAmount}; + nsString msg; + rv = bundle->FormatStringFromName("compactingDone", params, msg); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + mWindow->GetStatusFeedback(getter_AddRefs(statusFeedback)); + if (statusFeedback) { + statusFeedback->SetStatusString(msg); + } +} |