/* -*- 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 bundleService = mozilla::components::StringBundle::Service(); NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); nsCOMPtr 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 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 m_folder; // current folder being compact nsCOMPtr m_db; // new database for the compact folder nsCOMPtr m_file; // new mailbox for the compact folder nsCOMPtr m_fileStream; // output file stream for writing // All message keys that need to be copied over. nsTArray 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 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 m_messageService; // message service for // copying nsCOMPtr m_window; nsCOMPtr 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 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 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 mailDBFactory; nsresult rv = db->ListAllKeys(m_keys); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr 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 completionFn, nsIMsgWindow* msgWindow) { NS_ENSURE_ARG_POINTER(folder); m_completionFn = completionFn; m_window = msgWindow; nsresult rv; nsCOMPtr db; nsCOMPtr path; nsCString baseMessageURI; nsCOMPtr 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(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 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 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 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 bundle; rv = GetBaseStringBundle(getter_AddRefs(bundle)); if (NS_FAILED(rv)) break; AutoTArray 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 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 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 path; nsCOMPtr folderInfo; // get leaf name and database name of the folder nsresult rv = m_folder->GetFilePath(getter_AddRefs(path)); nsCOMPtr folderPath = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); rv = folderPath->InitWithFile(path); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr 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 newSummaryFile; rv = GetSummaryFileLocation(m_file, getter_AddRefs(newSummaryFile)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr transferInfo; m_folder->GetDBTransferInfo(getter_AddRefs(transferInfo)); // close down database of the original folder m_folder->ForceDBClosed(); nsCOMPtr 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 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 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 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 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 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 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 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 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(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 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 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 thisSupports; QueryInterface(NS_GET_IID(nsISupports), getter_AddRefs(thisSupports)); nsCOMPtr 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 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 uri; nsCOMPtr msgHdr; nsCOMPtr statusFeedback; nsCOMPtr 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 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 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 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 msgHdr; nsCOMPtr 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 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 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>& 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 folder = mQueue.PopLastElement(); // Sanity check - should we be compacting this folder? nsCOMPtr 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 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(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 bundle; nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle)); NS_ENSURE_SUCCESS_VOID(rv); nsAutoString expungedAmount; FormatFileSize(mTotalBytesGained, true, expungedAmount); AutoTArray params = {expungedAmount}; nsString msg; rv = bundle->FormatStringFromName("compactingDone", params, msg); NS_ENSURE_SUCCESS_VOID(rv); nsCOMPtr statusFeedback; mWindow->GetStatusFeedback(getter_AddRefs(statusFeedback)); if (statusFeedback) { statusFeedback->SetStatusString(msg); } }