/* -*- 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 "nsAutoPtr.h" #include "nsIFile.h" #include "nsNetUtil.h" #include "nsIMsgHdr.h" #include "nsIStreamListener.h" #include "nsIMsgMessageService.h" #include "nsMsgDBCID.h" #include "nsMsgUtils.h" #include "nsISeekableStream.h" #include "nsIDBFolderInfo.h" #include "nsIDocShell.h" #include "nsIPrompt.h" #include "nsIInterfaceRequestorUtils.h" #include "nsIMsgLocalMailFolder.h" #include "nsIMsgImapMailFolder.h" #include "nsMailHeaders.h" #include "nsMsgI18N.h" #include "prprf.h" #include "nsMsgLocalFolderHdrs.h" #include "nsIMsgDatabase.h" #include "nsArrayUtils.h" #include "nsMsgMessageFlags.h" #include "nsIMsgStatusFeedback.h" #include "nsMsgBaseCID.h" #include "nsIMsgFolderNotificationService.h" #include "nsIMsgPluggableStore.h" #include "nsMsgFolderCompactor.h" #include #include "nsIOutputStream.h" #include "nsIInputStream.h" #include "nsPrintfCString.h" ////////////////////////////////////////////////////////////////////////////// // nsFolderCompactState ////////////////////////////////////////////////////////////////////////////// NS_IMPL_ISUPPORTS(nsFolderCompactState, nsIMsgFolderCompactor, nsIRequestObserver, nsIStreamListener, nsICopyMessageStreamListener, nsIUrlListener) nsFolderCompactState::nsFolderCompactState() { m_fileStream = nullptr; m_size = 0; m_curIndex = 0; m_status = NS_OK; m_compactAll = false; m_compactOfflineAlso = false; m_compactingOfflineFolders = false; m_parsingFolder=false; m_folderIndex = 0; m_startOfMsg = true; m_needStatusLine = false; m_totalExpungedBytes = 0; m_alreadyWarnedDiskSpace = false; } 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_keyArray); NS_ENSURE_SUCCESS(rv, rv); m_size = m_keyArray->m_keys.Length(); nsCOMPtr msgDBService = do_GetService(NS_MSGDB_SERVICE_CONTRACTID, &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; } NS_IMETHODIMP nsFolderCompactState::CompactFolders(nsIArray *aArrayOfFoldersToCompact, nsIArray *aOfflineFolderArray, nsIUrlListener *aUrlListener, nsIMsgWindow *aMsgWindow) { m_window = aMsgWindow; m_listener = aUrlListener; if (aArrayOfFoldersToCompact) m_folderArray = aArrayOfFoldersToCompact; else if (aOfflineFolderArray) { m_folderArray = aOfflineFolderArray; m_compactingOfflineFolders = true; aOfflineFolderArray = nullptr; } if (!m_folderArray) return NS_OK; m_compactAll = true; m_compactOfflineAlso = aOfflineFolderArray != nullptr; if (m_compactOfflineAlso) m_offlineFolderArray = aOfflineFolderArray; m_folderIndex = 0; nsresult rv = NS_OK; nsCOMPtr firstFolder = do_QueryElementAt(m_folderArray, m_folderIndex, &rv); if (NS_SUCCEEDED(rv) && firstFolder) Compact(firstFolder, m_compactingOfflineFolders, aUrlListener, aMsgWindow); //start with first folder from here. return rv; } NS_IMETHODIMP nsFolderCompactState::Compact(nsIMsgFolder *folder, bool aOfflineStore, nsIUrlListener *aListener, nsIMsgWindow *aMsgWindow) { NS_ENSURE_ARG_POINTER(folder); m_listener = aListener; if (!m_compactingOfflineFolders && !aOfflineStore) { nsCOMPtr imapFolder = do_QueryInterface(folder); if (imapFolder) return imapFolder->Expunge(this, aMsgWindow); } m_window = aMsgWindow; 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_compactAll) return CompactNextFolder(); else 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 = do_QueryInterface(static_cast(this)); m_folder->AcquireSemaphore(supports); m_totalExpungedBytes += expunged; return StartCompacting(); } while(false); // block for easy skipping the compaction using 'break' folder->NotifyCompactCompleted(); if (m_compactAll) return CompactNextFolder(); else return NS_OK; } nsresult nsFolderCompactState::ShowStatusMsg(const nsString& aMsg) { nsCOMPtr statusFeedback; if (m_window) { m_window->GetStatusFeedback(getter_AddRefs(statusFeedback)); if (statusFeedback && !aMsg.IsEmpty()) return statusFeedback->SetStatusString(aMsg); } return NS_OK; } 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); // need to make sure the temp file goes in the same real directory // as the original file, so resolve sym links. m_file->SetFollowLinks(true); m_file->SetNativeLeafName(NS_LITERAL_CSTRING("nstmp")); rv = m_file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600); //make sure we are not crunching existing nstmp file NS_ENSURE_SUCCESS(rv, rv); m_window = aMsgWindow; m_keyArray = new nsMsgKeyArray; m_size = 0; 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; } NS_IMETHODIMP nsFolderCompactState::OnStopRunningUrl(nsIURI *url, nsresult status) { if (m_parsingFolder) { m_parsingFolder = false; if (NS_SUCCEEDED(status)) status = Compact(m_folder, m_compactingOfflineFolders, m_listener, m_window); else if (m_compactAll) CompactNextFolder(); } else if (m_compactAll) // this should be the imap case only { nsCOMPtr prevFolder = do_QueryElementAt(m_folderArray, m_folderIndex); if (prevFolder) prevFolder->SetMsgDatabase(nullptr); CompactNextFolder(); } else if (m_listener) { CompactCompleted(status); } return NS_OK; } nsresult nsFolderCompactState::StartCompacting() { nsCOMPtr msgStore; nsCOMPtr server; nsresult rv = m_folder->GetServer(getter_AddRefs(server)); NS_ENSURE_SUCCESS(rv, rv); rv = server->GetMsgStore(getter_AddRefs(msgStore)); NS_ENSURE_SUCCESS(rv, rv); // 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(NS_MSGNOTIFICATIONSERVICE_CONTRACTID)); if (notifier) notifier->NotifyItemEvent(m_folder, NS_LITERAL_CSTRING("FolderCompactStart"), nullptr); // TODO: test whether sorting the messages (m_keyArray) by messageOffset // would improve performance on large files (less seeks). // The m_keyArray 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_size > 0) { nsCOMPtr notUsed; ShowCompactingStatusMsg(); AddRef(); rv = m_messageService->CopyMessages(m_size, m_keyArray->m_keys.Elements(), m_folder, this, false, nullptr, m_window, getter_AddRefs(notUsed)); } else { // no messages to copy with FinishCompact(); // Release(); // we don't "own" ourselves yet. } 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); // need to make sure we put the .msf file in the same directory // as the original mailbox, so resolve symlinks. folderPath->SetFollowLinks(true); 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)) tempSummaryFile = nullptr; // flagging that a renamed db no longer exists 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(NS_MSGDB_SERVICE_CONTRACTID, &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); 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(NS_MSGNOTIFICATIONSERVICE_CONTRACTID)); if (notifier) notifier->NotifyItemEvent(m_folder, NS_LITERAL_CSTRING("FolderCompactFinish"), nullptr); m_folder->NotifyCompactCompleted(); if (m_compactAll) rv = CompactNextFolder(); else CompactCompleted(rv); return rv; } nsresult GetBaseStringBundle(nsIStringBundle **aBundle) { NS_ENSURE_ARG_POINTER(aBundle); nsCOMPtr bundleService = mozilla::services::GetStringBundleService(); NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); nsCOMPtr bundle; return bundleService->CreateBundle( "chrome://messenger/locale/messenger.properties", aBundle); } void nsFolderCompactState::CompactCompleted(nsresult exitCode) { NS_WARNING_ASSERTION(NS_SUCCEEDED(exitCode), "nsFolderCompactState::CompactCompleted failed"); if (m_listener) m_listener->OnStopRunningUrl(nullptr, exitCode); ShowDoneStatus(); } nsresult nsFolderCompactState::ReleaseFolderLock() { nsresult result = NS_OK; if (!m_folder) return result; bool haveSemaphore; nsCOMPtr supports = do_QueryInterface(static_cast(this)); result = m_folder->TestSemaphore(supports, &haveSemaphore); if(NS_SUCCEEDED(result) && haveSemaphore) result = m_folder->ReleaseSemaphore(supports); return result; } void nsFolderCompactState::ShowDoneStatus() { if (m_folder) { nsString statusString; nsCOMPtr bundle; nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle)); NS_ENSURE_SUCCESS_VOID(rv); nsAutoString expungedAmount; FormatFileSize(m_totalExpungedBytes, true, expungedAmount); const char16_t* params[] = { expungedAmount.get() }; rv = bundle->FormatStringFromName(u"compactingDone", params, 1, getter_Copies(statusString)); if (!statusString.IsEmpty() && NS_SUCCEEDED(rv)) ShowStatusMsg(statusString); } } nsresult nsFolderCompactState::CompactNextFolder() { m_folderIndex++; uint32_t cnt = 0; nsresult rv = m_folderArray->GetLength(&cnt); NS_ENSURE_SUCCESS(rv, rv); // m_folderIndex might be > cnt if we compact offline stores, // and get back here from OnStopRunningUrl. if (m_folderIndex >= cnt) { if (!m_compactOfflineAlso || m_compactingOfflineFolders) { CompactCompleted(NS_OK); return rv; } m_compactingOfflineFolders = true; nsCOMPtr folder = do_QueryElementAt(m_folderArray, m_folderIndex-1, &rv); if (NS_SUCCEEDED(rv) && folder) return folder->CompactAllOfflineStores(this, m_window, m_offlineFolderArray); else NS_WARNING("couldn't get folder to compact offline stores"); } nsCOMPtr folder = do_QueryElementAt(m_folderArray, m_folderIndex, &rv); if (NS_SUCCEEDED(rv) && folder) rv = Compact(folder, m_compactingOfflineFolders, m_listener, m_window); else CompactCompleted(rv); return rv; } nsresult nsFolderCompactState::GetMessage(nsIMsgDBHdr **message) { return GetMsgDBHdrFromURI(m_messageUri.get(), message); } NS_IMETHODIMP nsFolderCompactState::OnStartRequest(nsIRequest *request, nsISupports *ctxt) { return StartMessage(); } NS_IMETHODIMP nsFolderCompactState::OnStopRequest(nsIRequest *request, nsISupports *ctxt, nsresult status) { nsCOMPtr msgHdr; if (NS_FAILED(status)) { m_status = status; // set the m_status to status so the destructor can remove the // temp folder and database 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_size) { 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(); } } Release(); // kill self return status; } // XXX TODO: This function is sadly lacking all status checking, it always // returns NS_OK and moves onto the next message. NS_IMETHODIMP nsFolderCompactState::OnDataAvailable(nsIRequest *request, nsISupports *ctxt, nsIInputStream *inStr, uint64_t sourceOffset, uint32_t count) { if (!m_fileStream || !inStr) return NS_ERROR_FAILURE; nsresult rv = NS_OK; uint32_t msgFlags; bool checkForKeyword = m_startOfMsg; bool addKeywordHdr = false; uint32_t needToGrowKeywords = 0; uint32_t statusOffset; nsCString msgHdrKeywords; if (m_startOfMsg) { m_statusOffset = 0; m_addedHeaderSize = 0; m_messageUri.Truncate(); // clear the previous message uri if (NS_SUCCEEDED(BuildMessageURI(m_baseMessageUri.get(), m_keyArray->m_keys[m_curIndex], m_messageUri))) { rv = GetMessage(getter_AddRefs(m_curSrcHdr)); NS_ENSURE_SUCCESS(rv, rv); if (m_curSrcHdr) { (void) m_curSrcHdr->GetFlags(&msgFlags); (void) m_curSrcHdr->GetStatusOffset(&statusOffset); if (statusOffset == 0) m_needStatusLine = true; // x-mozilla-status lines should be at the start of the headers, and the code // below assumes everything will fit in m_dataBuffer - if there's not // room, skip the keyword stuff. if (statusOffset > sizeof(m_dataBuffer) - 1024) { checkForKeyword = false; NS_ASSERTION(false, "status offset past end of read buffer size"); } } } m_startOfMsg = false; } 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 status offset is past the number of bytes we read, it's probably bogus, // and we shouldn't do any of the keyword stuff. if (statusOffset + X_MOZILLA_STATUS_LEN > readCount) checkForKeyword = false; if (NS_SUCCEEDED(rv)) { if (checkForKeyword) { // make sure that status offset really points to x-mozilla-status line if (!strncmp(m_dataBuffer + statusOffset, X_MOZILLA_STATUS, X_MOZILLA_STATUS_LEN)) { const char *keywordHdr = PL_strnrstr(m_dataBuffer, HEADER_X_MOZILLA_KEYWORDS, readCount); if (keywordHdr) m_curSrcHdr->GetUint32Property("growKeywords", &needToGrowKeywords); else addKeywordHdr = true; m_curSrcHdr->GetStringProperty("keywords", getter_Copies(msgHdrKeywords)); } checkForKeyword = false; } uint32_t blockOffset = 0; if (m_needStatusLine) { m_needStatusLine = false; // we need to parse out the "From " header, write it out, then // write out the x-mozilla-status headers, and set the // status offset of the dest hdr for later use // in OnEndCopy). if (!strncmp(m_dataBuffer, "From ", 5)) { blockOffset = 5; // skip from line MsgAdvanceToNextLine(m_dataBuffer, blockOffset, readCount); char statusLine[50]; m_fileStream->Write(m_dataBuffer, blockOffset, &writeCount); m_statusOffset = blockOffset; PR_snprintf(statusLine, sizeof(statusLine), X_MOZILLA_STATUS_FORMAT MSG_LINEBREAK, msgFlags & 0xFFFF); m_fileStream->Write(statusLine, strlen(statusLine), &m_addedHeaderSize); PR_snprintf(statusLine, sizeof(statusLine), X_MOZILLA_STATUS2_FORMAT MSG_LINEBREAK, msgFlags & 0xFFFF0000); m_fileStream->Write(statusLine, strlen(statusLine), &bytesWritten); m_addedHeaderSize += bytesWritten; } else { NS_ASSERTION(false, "not an envelope"); // try to mark the db as invalid so it will be reparsed. nsCOMPtr srcDB; m_folder->GetMsgDatabase(getter_AddRefs(srcDB)); if (srcDB) { srcDB->SetSummaryValid(false); srcDB->ForceClosed(); } } } #define EXTRA_KEYWORD_HDR " " MSG_LINEBREAK // if status offset isn't in the first block, this code won't work. There's no good reason // for the status offset not to be at the beginning of the message anyway. if (addKeywordHdr) { // if blockOffset is set, we added x-mozilla-status headers so // file pointer is already past them. if (!blockOffset) { blockOffset = statusOffset; // skip x-mozilla-status and status2 lines. MsgAdvanceToNextLine(m_dataBuffer, blockOffset, readCount); MsgAdvanceToNextLine(m_dataBuffer, blockOffset, readCount); // need to rewrite the headers up to and including the x-mozilla-status2 header m_fileStream->Write(m_dataBuffer, blockOffset, &writeCount); } // we should write out the existing keywords from the msg hdr, if any. if (msgHdrKeywords.IsEmpty()) { // no keywords, so write blank header m_fileStream->Write(X_MOZILLA_KEYWORDS, sizeof(X_MOZILLA_KEYWORDS) - 1, &bytesWritten); m_addedHeaderSize += bytesWritten; } else { if (msgHdrKeywords.Length() < sizeof(X_MOZILLA_KEYWORDS) - sizeof(HEADER_X_MOZILLA_KEYWORDS) + 10 /* allow some slop */) { // keywords fit in normal blank header, so replace blanks in keyword hdr with keywords nsAutoCString keywordsHdr(X_MOZILLA_KEYWORDS); keywordsHdr.Replace(sizeof(HEADER_X_MOZILLA_KEYWORDS) + 1, msgHdrKeywords.Length(), msgHdrKeywords); m_fileStream->Write(keywordsHdr.get(), keywordsHdr.Length(), &bytesWritten); m_addedHeaderSize += bytesWritten; } else { // keywords don't fit, so write out keywords on one line and an extra blank line nsCString newKeywordHeader(HEADER_X_MOZILLA_KEYWORDS ": "); newKeywordHeader.Append(msgHdrKeywords); newKeywordHeader.Append(MSG_LINEBREAK EXTRA_KEYWORD_HDR); m_fileStream->Write(newKeywordHeader.get(), newKeywordHeader.Length(), &bytesWritten); m_addedHeaderSize += bytesWritten; } } addKeywordHdr = false; } else if (needToGrowKeywords) { blockOffset = statusOffset; if (!strncmp(m_dataBuffer + blockOffset, X_MOZILLA_STATUS, X_MOZILLA_STATUS_LEN)) MsgAdvanceToNextLine(m_dataBuffer, blockOffset, readCount); // skip x-mozilla-status hdr if (!strncmp(m_dataBuffer + blockOffset, X_MOZILLA_STATUS2, X_MOZILLA_STATUS2_LEN)) MsgAdvanceToNextLine(m_dataBuffer, blockOffset, readCount); // skip x-mozilla-status2 hdr uint32_t preKeywordBlockOffset = blockOffset; if (!strncmp(m_dataBuffer + blockOffset, HEADER_X_MOZILLA_KEYWORDS, sizeof(HEADER_X_MOZILLA_KEYWORDS) - 1)) { do { // skip x-mozilla-keywords hdr and any existing continuation headers MsgAdvanceToNextLine(m_dataBuffer, blockOffset, readCount); } while (m_dataBuffer[blockOffset] == ' '); } int32_t oldKeywordSize = blockOffset - preKeywordBlockOffset; // rewrite the headers up to and including the x-mozilla-status2 header m_fileStream->Write(m_dataBuffer, preKeywordBlockOffset, &writeCount); // let's just rewrite all the keywords on several lines and add a blank line, // instead of worrying about which are missing. bool done = false; nsAutoCString keywordHdr(HEADER_X_MOZILLA_KEYWORDS ": "); int32_t nextBlankOffset = 0; int32_t curHdrLineStart = 0; int32_t newKeywordSize = 0; while (!done) { nextBlankOffset = msgHdrKeywords.FindChar(' ', nextBlankOffset); if (nextBlankOffset == kNotFound) { nextBlankOffset = msgHdrKeywords.Length(); done = true; } if (nextBlankOffset - curHdrLineStart > 90 || done) { keywordHdr.Append(nsDependentCSubstring(msgHdrKeywords, curHdrLineStart, msgHdrKeywords.Length() - curHdrLineStart)); keywordHdr.Append(MSG_LINEBREAK); m_fileStream->Write(keywordHdr.get(), keywordHdr.Length(), &bytesWritten); newKeywordSize += bytesWritten; curHdrLineStart = nextBlankOffset; keywordHdr.Assign(' '); } nextBlankOffset++; } m_fileStream->Write(EXTRA_KEYWORD_HDR, sizeof(EXTRA_KEYWORD_HDR) - 1, &bytesWritten); newKeywordSize += bytesWritten; m_addedHeaderSize += newKeywordSize - oldKeywordSize; m_curSrcHdr->SetUint32Property("growKeywords", 0); needToGrowKeywords = false; writeCount += blockOffset - preKeywordBlockOffset; // fudge writeCount } if (readCount <= blockOffset) { NS_ASSERTION(false, "bad block offset"); // not sure what to do to handle this. } m_fileStream->Write(m_dataBuffer + blockOffset, readCount - blockOffset, &bytesWritten); writeCount += bytesWritten; count -= readCount; if (writeCount != readCount) return NS_MSG_ERROR_WRITING_MAIL_FOLDER; } } return rv; } nsOfflineStoreCompactState::nsOfflineStoreCompactState() { } 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_keyArray); m_size = m_keyArray->m_keys.Length(); 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_size) { // Filter out msgs that have the "pendingRemoval" attribute set. nsCOMPtr hdr; nsString pendingRemoval; nsresult rv = m_db->GetMsgHdrForKey(m_keyArray->m_keys[m_curIndex], getter_AddRefs(hdr)); NS_ENSURE_SUCCESS(rv, rv); hdr->GetProperty("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", ""); continue; } m_messageUri.Truncate(); // clear the previous message uri rv = BuildMessageURI(m_baseMessageUri.get(), m_keyArray->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.get(), 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; GetMessage(getter_AddRefs(hdr)); if (hdr) { uint32_t resultFlags; hdr->AndFlags(~nsMsgMessageFlags::Offline, &resultFlags); } m_curIndex++; continue; } else break; } done = m_curIndex >= m_size; // 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, nsISupports *ctxt, nsresult status) { nsresult rv = status; nsCOMPtr uri; nsCOMPtr msgHdr; nsCOMPtr statusFeedback; 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; uri = do_QueryInterface(ctxt, &rv); if (NS_FAILED(rv)) goto done; rv = GetMessage(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); char storeToken[100]; PR_snprintf(storeToken, sizeof(storeToken), "%lld", 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_size); } // 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(); Release(); // 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(); Release(); // kill self 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_compactAll) rv = CompactNextFolder(); return rv; } NS_IMETHODIMP nsFolderCompactState::Init(nsIMsgFolder *srcFolder, nsICopyMessageListener *destination, nsISupports *listenerData) { return NS_OK; } NS_IMETHODIMP nsFolderCompactState::StartMessage() { nsresult rv = NS_ERROR_FAILURE; NS_ASSERTION(m_fileStream, "Fatal, null m_fileStream...\n"); 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; } NS_IMETHODIMP nsFolderCompactState::EndCopy(nsISupports *url, nsresult aStatus) { nsCOMPtr msgHdr; nsCOMPtr newMsgHdr; if (m_curIndex >= m_size) { NS_ASSERTION(false, "m_curIndex out of bounds"); return NS_OK; } /* Messages need to have trailing blank lines */ uint32_t bytesWritten; (void) m_fileStream->Write(MSG_LINEBREAK, MSG_LINEBREAK_LEN, &bytesWritten); /** * 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) { if (m_statusOffset != 0) newMsgHdr->SetStatusOffset(m_statusOffset); char storeToken[100]; PR_snprintf(storeToken, sizeof(storeToken), "%lld", m_startOfNewMsg); newMsgHdr->SetStringProperty("storeToken", storeToken); newMsgHdr->SetMessageOffset(m_startOfNewMsg); uint32_t msgSize; (void) newMsgHdr->GetMessageSize(&msgSize); if (m_addedHeaderSize) { msgSize += m_addedHeaderSize; newMsgHdr->SetMessageSize(msgSize); } m_totalMsgSize += msgSize + MSG_LINEBREAK_LEN; } // m_db->Commit(nsMsgDBCommitType::kLargeCommit); // no sense commiting 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_size); } return NS_OK; } nsresult nsOfflineStoreCompactState::StartCompacting() { nsresult rv = NS_OK; if (m_size > 0 && m_curIndex == 0) { AddRef(); // 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, nsISupports *ctxt, nsIInputStream *inStr, uint64_t sourceOffset, uint32_t count) { if (!m_fileStream || !inStr) return NS_ERROR_FAILURE; nsresult rv = NS_OK; if (m_startOfMsg) { m_statusOffset = 0; m_offlineMsgSize = 0; m_messageUri.Truncate(); // clear the previous message uri if (NS_SUCCEEDED(BuildMessageURI(m_baseMessageUri.get(), m_keyArray->m_keys[m_curIndex], m_messageUri))) { rv = GetMessage(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; }