summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base/src/nsMsgFolderCompactor.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/base/src/nsMsgFolderCompactor.cpp')
-rw-r--r--comm/mailnews/base/src/nsMsgFolderCompactor.cpp1391
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);
+ }
+}