/* 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 "EwsFolder.h" #include "EwsFolderCopyHandler.h" #include "EwsListeners.h" #include "EwsMessageCopyHandler.h" #include "EwsCopyMoveTransaction.h" #include "IEwsClient.h" #include "IEwsIncomingServer.h" #include "ErrorList.h" #include "FolderCompactor.h" #include "FolderPopulation.h" #include "MailNewsTypes.h" #include "MsgOperationListener.h" #include "nsAutoSyncState.h" #include "nsIInputStream.h" #include "nsIMessenger.h" #include "mozilla/intl/Localization.h" #include "nsIMsgCopyService.h" #include "nsIMsgDBView.h" #include "nsIMsgDatabase.h" #include "nsIMsgFilterService.h" #include "nsIMsgFolderCacheElement.h" #include "nsIMsgFolderNotificationService.h" #include "nsIMsgPluggableStore.h" #include "nsIMsgStatusFeedback.h" #include "nsISpamSettings.h" #include "nsITransactionManager.h" #include "nsIMsgWindow.h" #include "nsString.h" #include "nsMsgFolderFlags.h" #include "nsMsgUtils.h" #include "nsNetUtil.h" #include "nsPrintfCString.h" #include "nsTArray.h" #include "nsTHashSet.h" #include "nscore.h" #include "OfflineStorage.h" #include "mozilla/Components.h" #include "mozilla/StaticPrefs_mail.h" #define kEWSRootURI "ews:/" #define kEWSMessageRootURI "ews-message:/" #define SYNC_STATE_PROPERTY "ewsSyncStateToken" using namespace mozilla; using namespace mozilla::StaticPrefs; extern LazyLogModule FILTERLOGMODULE; // From nsMsgFilterService. constexpr auto kEwsIdProperty = "ewsId"; nsresult CreateNewLocalEwsFolder(nsIMsgFolder* parent, const nsACString& ewsId, const nsACString& folderName, nsIMsgFolder** createdFolder) { nsCOMPtr msgStore; nsresult rv = parent->GetMsgStore(getter_AddRefs(msgStore)); NS_ENSURE_SUCCESS(rv, rv); // Initialize storage and memory for the new folder and register it with // the parent folder. nsCOMPtr newFolder; rv = msgStore->CreateFolder(parent, folderName, getter_AddRefs(newFolder)); NS_ENSURE_SUCCESS(rv, rv); // We've already verified that there's exactly one EWS ID in `ids`. rv = newFolder->SetStringProperty(kEwsIdProperty, ewsId); NS_ENSURE_SUCCESS(rv, rv); // Notify any consumers listening for updates regarding the folder's // creation. nsCOMPtr notifier = mozilla::components::FolderNotification::Service(); notifier->NotifyFolderAdded(newFolder); rv = parent->NotifyFolderAdded(newFolder); NS_ENSURE_SUCCESS(rv, rv); newFolder.forget(createdFolder); return NS_OK; } static nsresult GetEwsIdsForMessageHeaders( const nsTArray>& messageHeaders, nsTArray& ewsIds) { nsresult rv; for (const auto& header : messageHeaders) { nsCString ewsId; rv = header->GetStringProperty(kEwsIdProperty, ewsId); NS_ENSURE_SUCCESS(rv, rv); if (ewsId.IsEmpty()) { nsMsgKey messageKey; rv = header->GetMessageKey(&messageKey); NS_ENSURE_SUCCESS(rv, rv); NS_WARNING( nsPrintfCString("Skipping header without EWS ID. messageKey=%d", messageKey) .get()); continue; } ewsIds.AppendElement(ewsId); } return NS_OK; } static nsresult NotifyMessageCopyServiceComplete( nsIMsgFolder* sourceFolder, nsIMsgFolder* destinationFolder, nsresult status) { nsCOMPtr copyService = mozilla::components::Copy::Service(); return copyService->NotifyCompletion(sourceFolder, destinationFolder, status); } static nsresult HandleMoveError(nsIMsgFolder* sourceFolder, nsIMsgFolder* destinationFolder, nsresult status) { NS_ERROR(nsPrintfCString("EWS same-server move error: %s", mozilla::GetStaticErrorName(status)) .get()); sourceFolder->NotifyFolderEvent(kDeleteOrMoveMsgFailed); return NotifyMessageCopyServiceComplete(sourceFolder, destinationFolder, status); } // Return a scope guard that will ensure the copy service is notified of failure // when the calling scope exits with the specified `nsresult` in a failed state. [[nodiscard]] static auto GuardCopyServiceExit(nsIMsgFolder* sourceFolder, nsIMsgFolder* destinationFolder, const nsresult& rv) { return mozilla::MakeScopeExit([sourceFolder, destinationFolder, &rv]() { if (NS_FAILED(rv)) { sourceFolder->NotifyFolderEvent(kDeleteOrMoveMsgFailed); NotifyMessageCopyServiceComplete(sourceFolder, destinationFolder, rv); } }); } /// Return a guard that will ensure the specified `listener` is notified when /// the calling scope exits. [[nodiscard]] static auto GuardCopyServiceListener( nsIMsgCopyServiceListener* listener, const nsresult& rv) { if (listener) { listener->OnStartCopy(); } return mozilla::MakeScopeExit([&rv, listener]() { if (listener) { listener->OnStopCopy(rv); } }); } NS_IMPL_ISUPPORTS_INHERITED(EwsFolder, nsMsgDBFolder, IEwsFolder); EwsFolder::EwsFolder() : mHasLoadedSubfolders(false) {} EwsFolder::~EwsFolder() = default; nsresult EwsFolder::CreateBaseMessageURI(const nsACString& aURI) { nsAutoCString tailURI(aURI); // Remove the scheme and the following `:/'. if (tailURI.Find(kEWSRootURI) == 0) { tailURI.Cut(0, PL_strlen(kEWSRootURI)); } mBaseMessageURI = kEWSMessageRootURI; mBaseMessageURI += tailURI; return NS_OK; } nsresult EwsFolder::GetDatabase() { // No default implementation of this, even though it seems to be pretty // protocol agnostic. Cribbed from `nsImapMailFolder.cpp`. if (!mDatabase) { nsresult rv; nsCOMPtr msgDBService = do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); NS_ENSURE_SUCCESS(rv, rv); // Create the database, blowing it away if it needs to be rebuilt. rv = msgDBService->OpenFolderDB(this, false, getter_AddRefs(mDatabase)); if (NS_FAILED(rv)) { rv = msgDBService->CreateNewDB(this, getter_AddRefs(mDatabase)); } NS_ENSURE_SUCCESS(rv, rv); UpdateNewMessages(); if (mAddListener) { mDatabase->AddListener(this); } UpdateSummaryTotals(true); } return NS_OK; } NS_IMETHODIMP EwsFolder::CreateStorageIfMissing(nsIUrlListener* urlListener) { NS_WARNING("CreateStorageIfMissing"); return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP EwsFolder::CreateSubfolder(const nsACString& aFolderName, nsIMsgWindow* msgWindow) { nsCString ewsId; nsresult rv = GetEwsId(ewsId); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr client; rv = GetEwsClient(getter_AddRefs(client)); NS_ENSURE_SUCCESS(rv, rv); const auto folderName = nsCString(aFolderName); RefPtr listener = new EwsSimpleListener( [self = RefPtr(this), folderName](const nsTArray& ids, bool useLegacyFallback) { NS_ENSURE_TRUE(ids.Length() == 1, NS_ERROR_UNEXPECTED); nsCOMPtr newFolder; return CreateNewLocalEwsFolder(self, ids[0], folderName, getter_AddRefs(newFolder)); }); return client->CreateFolder(listener, ewsId, folderName); } NS_IMETHODIMP EwsFolder::GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo, nsIMsgDatabase** database) { // No default implementation of this, even though it seems to be pretty // protocol agnostic. Cribbed from `nsImapMailFolder.cpp`. NS_ENSURE_ARG_POINTER(folderInfo); NS_ENSURE_ARG_POINTER(database); // Ensure that our cached database handle is initialized. nsresult rv = GetDatabase(); NS_ENSURE_SUCCESS(rv, rv); NS_ADDREF(*database = mDatabase); return (*database)->GetDBFolderInfo(folderInfo); } NS_IMETHODIMP EwsFolder::GetSupportsOffline(bool* supportsOffline) { NS_ENSURE_ARG_POINTER(supportsOffline); if (mFlags & nsMsgFolderFlags::Virtual) { *supportsOffline = false; } else { // Non-virtual EWS folders support downloading messages for offline use. *supportsOffline = true; } return NS_OK; } NS_IMETHODIMP EwsFolder::GetIncomingServerType(nsACString& aServerType) { aServerType.AssignLiteral("ews"); return NS_OK; } NS_IMETHODIMP EwsFolder::GetNewMessages(nsIMsgWindow* aWindow, nsIUrlListener* aListener) { // Sync the message list. We don't need to sync the folder tree, because the // only likely consumer of this method is `EwsIncomingServer`, which does this // before asking folders to sync their message lists. return SyncMessages(aWindow, aListener); } NS_IMETHODIMP EwsFolder::GetSubFolders( nsTArray>& aSubFolders) { // The first time we ask for a list of subfolders, this folder has no idea // what they are. Use the message store to get a list, which we cache in this // folder's memory. (Keeping it up-to-date is managed by the `AddSubfolder` // and `CreateSubfolder`, where appropriate.) if (!mHasLoadedSubfolders) { // If we fail this time, we're unlikely to succeed later, so we set this // first thing. mHasLoadedSubfolders = true; nsCOMPtr server; nsresult rv = GetServer(getter_AddRefs(server)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr msgStore; rv = server->GetMsgStore(getter_AddRefs(msgStore)); NS_ENSURE_SUCCESS(rv, rv); // Running discovery on the message store will populate the subfolder list. rv = msgStore->DiscoverSubFolders(this, true); NS_ENSURE_SUCCESS(rv, rv); } return nsMsgDBFolder::GetSubFolders(aSubFolders); } NS_IMETHODIMP EwsFolder::RenameSubFolders(nsIMsgWindow* msgWindow, nsIMsgFolder* oldFolder) { NS_WARNING("RenameSubFolders"); return NS_ERROR_NOT_IMPLEMENTED; } NS_IMETHODIMP EwsFolder::MarkMessagesRead( const nsTArray>& messages, bool markRead) { nsCOMPtr client; nsresult rv = GetEwsClient(getter_AddRefs(client)); NS_ENSURE_SUCCESS(rv, rv); CopyableTArray requestedIds(messages.Length()); // Get a list of the EWS IDs for the messages to be modified. for (const auto& msg : messages) { nsAutoCString itemId; rv = msg->GetStringProperty(kEwsIdProperty, itemId); NS_ENSURE_SUCCESS(rv, rv); requestedIds.AppendElement(itemId); } CopyableTArray> headersCopy(messages.Length()); headersCopy.AppendElements(messages); RefPtr listener = new EwsSimpleListener( [self = RefPtr(this), headersCopy = std::move(headersCopy), requestedIds, markRead](const nsTArray& ids, bool useLegacyFallback) mutable { MOZ_ASSERT(headersCopy.Length() == requestedIds.Length()); CopyableTArray> foundHeaders; if (requestedIds.Length() == ids.Length()) { // Assume all the returned ids are the ones we requested. foundHeaders = std::move(headersCopy); } else { // Not all requested messages were succesfully marked. // Find the ones that were. nsTHashSet returnedIds(ids.Length()); for (const auto& id : ids) { returnedIds.Insert(id); } foundHeaders.SetCapacity(ids.Length()); for (size_t i = 0; i < headersCopy.Length(); ++i) { if (returnedIds.Contains(requestedIds[i])) { foundHeaders.AppendElement(headersCopy[i]); } } } nsresult rv = self->nsMsgDBFolder::MarkMessagesRead(foundHeaders, markRead); NS_ENSURE_SUCCESS(rv, rv); rv = self->GetDatabase(); if (NS_SUCCEEDED(rv)) { self->mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); } return rv; }); return client->ChangeReadStatus(listener, requestedIds, markRead); } NS_IMETHODIMP EwsFolder::MarkAllMessagesRead(nsIMsgWindow* aMsgWindow) { nsCOMPtr client; MOZ_TRY(GetEwsClient(getter_AddRefs(client))); nsCString folderId; MOZ_TRY(GetEwsId(folderId)); nsTArray folderIds{{folderId}}; RefPtr listener = new EwsSimpleListener( [self = RefPtr(this), window = RefPtr(aMsgWindow)]( const nsTArray& ids, bool useLegacyFallback) { nsresult rv = self->GetDatabase(); NS_ENSURE_SUCCESS(rv, rv); if (!useLegacyFallback) { // server marked read on its end, mark as read locally and set up undo nsTArray thoseMarked; rv = self->EnableNotifications(allMessageCountNotifications, false); NS_ENSURE_SUCCESS(rv, rv); rv = self->mDatabase->MarkAllRead(thoseMarked); nsresult rv2 = self->EnableNotifications(allMessageCountNotifications, true); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_SUCCESS(rv2, rv2); if (thoseMarked.Length() > 0) { rv = self->mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); NS_ENSURE_SUCCESS(rv, rv); if (window) { rv = self->AddMarkAllReadUndoAction( window, thoseMarked.Elements(), thoseMarked.Length()); NS_ENSURE_SUCCESS(rv, rv); } } } else { // server doesn't support marking folders, find unread messages and do // it manually nsCOMPtr hdrs; rv = self->mDatabase->EnumerateMessages(getter_AddRefs(hdrs)); NS_ENSURE_SUCCESS(rv, rv); nsTArray> unread; bool hasMore = false; while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) { nsCOMPtr msg; rv = hdrs->GetNext(getter_AddRefs(msg)); NS_ENSURE_SUCCESS(rv, rv); bool isRead; rv = msg->GetIsRead(&isRead); NS_ENSURE_SUCCESS(rv, rv); if (!isRead) { unread.AppendElement(msg); } } if (unread.Length() > 0) { rv = self->MarkMessagesRead(unread, true); } } return rv; }); return client->ChangeReadStatusAll(listener, folderIds, true, true); } NS_IMETHODIMP EwsFolder::UpdateFolder(nsIMsgWindow* aWindow) { // Sync the message list. // TODO: In the future, we might want to sync the folder hierarchy. Since // we already keep the local folder list quite in sync with remote operations, // and we already sync it in a couple of occurrences (when getting new // messages, performing biff, etc.), it's likely fine to leave this as a // future improvement. return SyncMessages(aWindow, nullptr); } NS_IMETHODIMP EwsFolder::Rename(const nsACString& aNewName, nsIMsgWindow* msgWindow) { nsAutoCString currentName; nsresult rv = GetName(currentName); NS_ENSURE_SUCCESS(rv, rv); // If the name hasn't changed, then avoid generating network traffic. if (aNewName.Equals(currentName)) { return NS_OK; } bool updatable = false; rv = GetCanRename(&updatable); NS_ENSURE_SUCCESS(rv, rv); if (!updatable) { return NS_ERROR_UNEXPECTED; } nsCOMPtr client; rv = GetEwsClient(getter_AddRefs(client)); NS_ENSURE_SUCCESS(rv, rv); nsAutoCString folderId; rv = GetEwsId(folderId); NS_ENSURE_SUCCESS(rv, rv); nsAutoCString syncStateToken; rv = GetStringProperty(SYNC_STATE_PROPERTY, syncStateToken); NS_ENSURE_SUCCESS(rv, rv); const nsCOMPtr window = msgWindow; const auto newName = nsCString(aNewName); RefPtr listener = new EwsSimpleListener( [self = RefPtr(this), newName, window](const nsTArray& ids, bool useLegacyFallback) { nsCOMPtr parentFolder; nsresult rv = self->GetParent(getter_AddRefs(parentFolder)); NS_ENSURE_SUCCESS(rv, rv); return LocalRenameOrReparentFolder(self, parentFolder, newName, window); }); return client->UpdateFolder(listener, folderId, aNewName); } NS_IMETHODIMP EwsFolder::CopyFileMessage( nsIFile* aFile, nsIMsgDBHdr* msgToReplace, bool isDraftOrTemplate, uint32_t newMsgFlags, const nsACString& aNewMsgKeywords, nsIMsgWindow* msgWindow, nsIMsgCopyServiceListener* copyListener) { // Ensure both a source file and a listener have been provided. NS_ENSURE_ARG_POINTER(aFile); NS_ENSURE_ARG_POINTER(copyListener); // Instantiate a `MessageCopyHandler` for this operation. nsCString ewsId; nsresult rv = GetEwsId(ewsId); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr client; rv = GetEwsClient(getter_AddRefs(client)); NS_ENSURE_SUCCESS(rv, rv); RefPtr handler = new MessageCopyHandler( aFile, this, isDraftOrTemplate, msgWindow, ewsId, client, copyListener); // Start copying the message. Once it has finished, `MessageCopyHandler` will // take care of sending the relevant notifications. rv = handler->StartCopyingNextMessage(); if (NS_FAILED(rv)) { // If setting up the operation has failed, send the relevant notifications // before exiting. handler->OnCopyCompleted(rv); } return rv; } NS_IMETHODIMP EwsFolder::CopyMessages( nsIMsgFolder* aSrcFolder, nsTArray> const& aSrcHdrs, bool aIsMove, nsIMsgWindow* aMsgWindow, nsIMsgCopyServiceListener* aCopyListener, bool aIsFolder, bool aAllowUndo) { NS_ENSURE_ARG_POINTER(aSrcFolder); nsresult rv = NS_OK; auto notifyFailureOnExit = GuardCopyServiceExit(aSrcFolder, this, rv); // Make sure we're not moving/copying to the root folder for the server, // since it cannot hold messages. bool isServer; MOZ_TRY(GetIsServer(&isServer)); if (isServer) { NS_ERROR("Destination is the root folder. Cannot move/copy here"); return NS_ERROR_FILE_COPY_OR_MOVE_FAILED; } bool isSameServer = false; rv = FoldersOnSameServer(aSrcFolder, this, &isSameServer); NS_ENSURE_SUCCESS(rv, rv); if (isSameServer) { // Since the folders are on the same (EWS) server, the other folder must // also be an EWS Folder. nsCOMPtr ewsSourceFolder{do_QueryInterface(aSrcFolder, &rv)}; NS_ENSURE_SUCCESS(rv, rv); const auto undoType = aIsMove ? nsIMessenger::eMoveMsg : nsIMessenger::eCopyMsg; rv = CopyItemsOnSameServer(ewsSourceFolder, aSrcHdrs, aIsMove, aMsgWindow, aCopyListener, aAllowUndo, undoType, nullptr); NS_ENSURE_SUCCESS(rv, rv); } else { // Cross-server copy or move. Instantiate a `MessageCopyHandler` for this // operation. nsCString ewsId; nsresult rv = GetEwsId(ewsId); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr client; MOZ_TRY(GetEwsClient(getter_AddRefs(client))); RefPtr handler = new MessageCopyHandler(aSrcFolder, this, aSrcHdrs, aIsMove, aMsgWindow, ewsId, client, aCopyListener); // Start the copy for the first message. Once this copy has finished, the // `MessageCopyHandler` will automatically start the copy for the next // message in line, and so on until every message in `srcHdrs` have been // copied. rv = handler->StartCopyingNextMessage(); if (NS_FAILED(rv)) { // If setting up the operation has failed, send the relevant notifications // before exiting. handler->OnCopyCompleted(rv); } NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } NS_IMETHODIMP EwsFolder::CopyItemsOnSameServer( IEwsFolder* aSrcFolder, nsTArray> const& aSrcHdrs, bool aIsMove, nsIMsgWindow* aMsgWindow, nsIMsgCopyServiceListener* aCopyListener, bool aAllowUndo, int32_t undoOperationType, IEwsFolderOperationListener* aOperationListener) { // Same server copy or move, perform operation remotely. nsTArray ewsIds; nsresult rv = GetEwsIdsForMessageHeaders(aSrcHdrs, ewsIds); NS_ENSURE_SUCCESS(rv, rv); nsAutoCString destinationFolderId; rv = GetEwsId(destinationFolderId); NS_ENSURE_SUCCESS(rv, rv); const nsCOMPtr msgWindow = aMsgWindow; const nsCOMPtr srcFolder = aSrcFolder; const nsCOMPtr copyListener = aCopyListener; const nsCOMPtr operationListener = aOperationListener; const RefPtr listener = new EwsSimpleFailibleMessageListener( aSrcHdrs, [self = RefPtr(this), srcFolder, msgWindow, aIsMove, copyListener, aAllowUndo, operationListener, undoOperationType]( const nsTArray>& srcHdrs, const nsTArray& ids, bool useLegacyFallback) MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA { nsresult rv = NS_OK; auto listenerExitGuard = GuardCopyServiceListener(copyListener, rv); nsCOMPtr genericFolder{ do_QueryInterface(srcFolder, &rv)}; NS_ENSURE_SUCCESS(rv, rv); nsTArray> newHeaders; if (useLegacyFallback) { rv = self->SyncMessages(msgWindow, nullptr); NS_ENSURE_SUCCESS(rv, rv); } else { // The new IDs were returned from the server. In this case, // the order of the new IDs will correspond to the order of // the input IDs specified in the initial request. NS_ENSURE_TRUE(ids.Length() == srcHdrs.Length(), NS_ERROR_UNEXPECTED); /// Copy the messages into the destination folder. rv = LocalCopyMessages(genericFolder, self, srcHdrs, newHeaders); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_TRUE(newHeaders.Length() == ids.Length(), NS_ERROR_UNEXPECTED); // Set the EWS ID property for each of the new headers. for (std::size_t i = 0; i < ids.Length(); ++i) { newHeaders[i]->SetStringProperty(kEwsIdProperty, ids[i]); } nsCOMPtr notifier = mozilla::components::FolderNotification::Service(); rv = notifier->NotifyMsgsMoveCopyCompleted(aIsMove, srcHdrs, self, newHeaders); NS_ENSURE_SUCCESS(rv, rv); } // If required, delete the original items from the source // folder. if (aIsMove) { rv = LocalDeleteMessages(genericFolder, srcHdrs); NS_ENSURE_SUCCESS(rv, rv); genericFolder->NotifyFolderEvent(kDeleteOrMoveMsgCompleted); } if (aAllowUndo && msgWindow) { nsCOMPtr transactionManager; rv = msgWindow->GetTransactionManager( getter_AddRefs(transactionManager)); NS_ENSURE_SUCCESS(rv, rv); RefPtr undoTransaction = aIsMove ? EwsCopyMoveTransaction::ForMove( srcFolder, self.get(), msgWindow, newHeaders.Clone()) : EwsCopyMoveTransaction::ForCopy( srcFolder, self.get(), msgWindow, srcHdrs.Clone(), newHeaders.Clone()); undoTransaction->SetTransactionType( static_cast(undoOperationType)); rv = transactionManager->DoTransaction(undoTransaction); NS_ENSURE_SUCCESS(rv, rv); } if (operationListener) { rv = operationListener->OnComplete(newHeaders); NS_ENSURE_SUCCESS(rv, rv); } return NotifyMessageCopyServiceComplete(genericFolder, self, NS_OK); }, [self = RefPtr(this), srcFolder, copyListener](nsresult status) { if (copyListener) { copyListener->OnStopCopy(status); } nsresult rv; nsCOMPtr genericFolder{ do_QueryInterface(srcFolder, &rv)}; NS_ENSURE_SUCCESS(rv, rv); return HandleMoveError(genericFolder, self, status); }); nsCOMPtr client; MOZ_TRY(GetEwsClient(getter_AddRefs(client))); if (aIsMove) { rv = client->MoveItems(listener, destinationFolderId, ewsIds); } else { rv = client->CopyItems(listener, destinationFolderId, ewsIds); } NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } static nsresult CompleteCopyMoveFolderOperation( nsIMsgFolder* srcFolder, nsIMsgFolder* destinationFolder, nsIMsgCopyServiceListener* copyListener, const nsACString& name, const nsCString& newEwsId) { nsresult rv = NS_OK; auto listenerExitGuard = GuardCopyServiceListener(copyListener, rv); nsCOMPtr newFolder; rv = destinationFolder->GetChildNamed(name, getter_AddRefs(newFolder)); NS_ENSURE_SUCCESS(rv, rv); if (!newFolder) { // Ensure the exit error state is set appropriately for the listener exit // guard. rv = NS_ERROR_UNEXPECTED; return rv; } newFolder->SetStringProperty(kEwsIdProperty, newEwsId); return NotifyMessageCopyServiceComplete(srcFolder, destinationFolder, NS_OK); } NS_IMETHODIMP EwsFolder::CopyFolder(nsIMsgFolder* aSrcFolder, bool aIsMoveFolder, nsIMsgWindow* aMsgWindow, nsIMsgCopyServiceListener* aCopyListener) { NS_ENSURE_ARG_POINTER(aSrcFolder); nsresult rv = NS_OK; auto notifyFailureOnExit = GuardCopyServiceExit(aSrcFolder, this, rv); nsCOMPtr client; MOZ_TRY(GetEwsClient(getter_AddRefs(client))); bool isSameServer; rv = FoldersOnSameServer(aSrcFolder, this, &isSameServer); NS_ENSURE_SUCCESS(rv, rv); if (isSameServer) { // Same server move or copy. rv = CopyFolderOnSameServer(aSrcFolder, aIsMoveFolder, aMsgWindow, aCopyListener); NS_ENSURE_SUCCESS(rv, rv); } else { // Cross-server folder move (or copy). Instantiate a `FolderCopyHandler` for // this operation. RefPtr handler = new FolderCopyHandler( aSrcFolder, this, aIsMoveFolder, aMsgWindow, client, aCopyListener); rv = handler->CopyNextFolder(); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } NS_IMETHODIMP EwsFolder::CopyFolderOnSameServer( nsIMsgFolder* aSourceFolder, bool aIsMoveFolder, nsIMsgWindow* aWindow, nsIMsgCopyServiceListener* aCopyListener) { nsAutoCString sourceEwsId; nsresult rv = aSourceFolder->GetStringProperty(kEwsIdProperty, sourceEwsId); NS_ENSURE_SUCCESS(rv, rv); if (sourceEwsId.IsEmpty()) { NS_ERROR("Expected EWS folder for server but folder has no EWS ID."); return NS_ERROR_UNEXPECTED; } nsAutoCString destinationEwsId; rv = GetEwsId(destinationEwsId); NS_ENSURE_SUCCESS(rv, rv); const nsCOMPtr srcFolder = aSourceFolder; const nsCOMPtr msgWindow = aWindow; const nsCOMPtr copyListener = aCopyListener; RefPtr listener = new EwsSimpleFailibleListener( [self = RefPtr(this), srcFolder, copyListener, msgWindow, aIsMoveFolder]( const nsTArray& ids, bool useLegacyFallback) { NS_ENSURE_TRUE(ids.Length() == 1, NS_ERROR_UNEXPECTED); const auto& newEwsId = ids[0]; nsAutoCString name; nsresult rv = srcFolder->GetName(name); NS_ENSURE_SUCCESS(rv, rv); // For a move, the EWS IDs of any subfolders or items of the moved // folder or subfolder are stable (no known documentation, so this is // through observation), so we can move in local storage to avoid a // sync. When copying, however, new EWS IDs must be created for any // subfolders or items of the copied folder or its subfolders, and we // have no way to obtain the new IDs other than performing a sync of // the folder hierarchy. if (aIsMoveFolder) { rv = LocalRenameOrReparentFolder(srcFolder, self, name, msgWindow); NS_ENSURE_SUCCESS(rv, rv); rv = CompleteCopyMoveFolderOperation(srcFolder, self, copyListener, name, newEwsId); return rv; } nsCOMPtr server; rv = self->GetServer(getter_AddRefs(server)); NS_ENSURE_SUCCESS(rv, rv); const nsCOMPtr ewsServer{ do_QueryInterface(server, &rv)}; NS_ENSURE_SUCCESS(rv, rv); // Limiting granularity of the folder hierarchy update to only the // destination folder is not possible due to our current strategy of // managing the EWS sync token at the server level for folder updates, // so we have to sync the entire hierarchy. In addition, for the // copied folder messages to appear, an additional message sync will // be required for the newly copied folder. However, to limit the // complexity of this callback chain, we don't perform that step here, // instead relying on external processes (such as folder // expansion/selection) to perform that operation in the future. // See https://bugzilla.mozilla.org/show_bug.cgi?id=1980963 // for the enhancement to improve hierarchy update granularity. const RefPtr listener = new EwsSimpleListener{ [self, srcFolder, msgWindow, copyListener, newEwsId, name]( const auto& ids, bool resyncRequired) { nsresult rv = NS_OK; rv = CompleteCopyMoveFolderOperation( srcFolder, self, copyListener, name, newEwsId); return rv; }}; return ewsServer->SyncFolderHierarchy(listener, msgWindow); }, [self = RefPtr(this), srcFolder, copyListener](nsresult status) { if (copyListener) { copyListener->OnStopCopy(status); } return HandleMoveError(srcFolder, self, status); }); nsCOMPtr client; MOZ_TRY(GetEwsClient(getter_AddRefs(client))); if (aIsMoveFolder) { rv = client->MoveFolders(listener, destinationEwsId, {sourceEwsId}); } else { rv = client->CopyFolders(listener, destinationEwsId, {sourceEwsId}); } NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult EwsFolder::HandleDeleteOperation( bool forceHardDelete, std::function&& onHardDelete, std::function&& onSoftDelete) { using DeleteModel = IEwsIncomingServer::DeleteModel; nsresult rv; bool isTrashFolder = mFlags & nsMsgFolderFlags::Trash; // Check the delete model to see if this should be a permanent delete. nsCOMPtr server; rv = GetServer(getter_AddRefs(server)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr ewsServer{do_QueryInterface(server, &rv)}; NS_ENSURE_SUCCESS(rv, rv); DeleteModel deleteModel; rv = ewsServer->GetDeleteModel(&deleteModel); if (forceHardDelete || isTrashFolder || deleteModel == DeleteModel::PERMANENTLY_DELETE) { return onHardDelete(); } // We're moving the messages to trash folder. nsCOMPtr trashFolder; rv = GetTrashFolder(getter_AddRefs(trashFolder)); NS_ENSURE_SUCCESS(rv, rv); if (!trashFolder) { return NS_ERROR_UNEXPECTED; } nsCOMPtr ewsTrashFolder = do_QueryInterface(trashFolder, &rv); NS_ENSURE_SUCCESS(rv, rv); return onSoftDelete(ewsTrashFolder); } NS_IMETHODIMP EwsFolder::DeleteMessages( nsTArray> const& aMsgHeaders, nsIMsgWindow* aMsgWindow, bool aDeleteStorage, bool aIsMove, nsIMsgCopyServiceListener* aCopyListener, bool aAllowUndo) { // If we're performing a "hard" delete, or if we're deleting from the trash // folder, perform a "real" deletion (i.e. delete the messages from both the // storage and the server). const auto headers = CopyableTArray>{aMsgHeaders}; const auto onHardDelete = [self = RefPtr(this), window = RefPtr(aMsgWindow), copyListener = RefPtr(aCopyListener), headers]() { nsCOMPtr feedback = nullptr; nsresult rv = NS_OK; if (window) { // Format the message we'll show the user while we wait for the remote // operation to complete. RefPtr l10n = intl::Localization::Create( {"messenger/activityFeedback.ftl"_ns}, true); auto l10nArgs = dom::Optional(); l10nArgs.Construct(); nsCString folderName; rv = self->GetLocalizedName(folderName); NS_ENSURE_SUCCESS(rv, rv); auto numberArg = l10nArgs.Value().Entries().AppendElement(); numberArg->mKey = "number"_ns; numberArg->mValue.SetValue().SetAsUTF8String().Assign( nsPrintfCString("%zu", headers.Length())); auto folderArg = l10nArgs.Value().Entries().AppendElement(); folderArg->mKey = "folderName"_ns; folderArg->mValue.SetValue().SetAsUTF8String().Assign(folderName); ErrorResult error; nsCString message; l10n->FormatValueSync("deleting-message"_ns, l10nArgs, message, error); // Show the formatted message in the status bar. rv = window->GetStatusFeedback(getter_AddRefs(feedback)); NS_ENSURE_SUCCESS(rv, rv); rv = feedback->ShowStatusString(NS_ConvertUTF8toUTF16(message)); NS_ENSURE_SUCCESS(rv, rv); rv = feedback->StartMeteors(); NS_ENSURE_SUCCESS(rv, rv); } // Define the listener with a success lambda callback, and start the // remote operation. RefPtr listener = new EwsSimpleMessageListener( headers, [self, copyListener, feedback]( const nsTArray>& srcHdrs, const nsTArray& ids, bool useLegacyFallback) { nsresult rv = NS_OK; auto listenerExitGuard = GuardCopyServiceListener(copyListener, rv); rv = LocalDeleteMessages(self, srcHdrs); NS_ENSURE_SUCCESS(rv, rv); if (feedback) { // Reset the status bar. return feedback->StopMeteors(); } return NS_OK; }); nsCOMPtr client; rv = self->GetEwsClient(getter_AddRefs(client)); NS_ENSURE_SUCCESS(rv, rv); nsTArray ewsIds; rv = GetEwsIdsForMessageHeaders(headers, ewsIds); NS_ENSURE_SUCCESS(rv, rv); return client->DeleteMessages(listener, ewsIds); }; // We're moving the messages to trash folder. const auto onSoftDelete = [self = RefPtr(this), headers, window = RefPtr(aMsgWindow), copyListener = RefPtr(aCopyListener)](IEwsFolder* trashFolder) { return trashFolder->CopyItemsOnSameServer( self, headers, true, window, copyListener, true, nsIMessenger::eDeleteMsg, nullptr); }; return HandleDeleteOperation(aDeleteStorage, std::move(onHardDelete), std::move(onSoftDelete)); } NS_IMETHODIMP EwsFolder::DeleteSelf(nsIMsgWindow* aWindow) { bool deletable = false; nsresult rv = GetDeletable(&deletable); NS_ENSURE_SUCCESS(rv, rv); if (!deletable) { return NS_ERROR_UNEXPECTED; } nsCString folderId; rv = GetEwsId(folderId); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr window = aWindow; const auto onHardDelete = [self = RefPtr(this), window, folderId]() { RefPtr listener = new EwsSimpleListener( [self, window](const nsTArray& ids, bool useLegacyFallback) { return self->nsMsgDBFolder::DeleteSelf(window); }); nsCOMPtr client; nsresult rv = self->GetEwsClient(getter_AddRefs(client)); NS_ENSURE_SUCCESS(rv, rv); return client->DeleteFolder(listener, {folderId}); }; const auto onSoftDelete = [self = RefPtr(this), window = RefPtr(aWindow)](IEwsFolder* trashFolder) { return trashFolder->CopyFolderOnSameServer(self, true, window, nullptr); }; return HandleDeleteOperation(false, std::move(onHardDelete), std::move(onSoftDelete)); } NS_IMETHODIMP EwsFolder::GetDeletable(bool* deletable) { NS_ENSURE_ARG_POINTER(deletable); bool isServer; nsresult rv = GetIsServer(&isServer); NS_ENSURE_SUCCESS(rv, rv); *deletable = !(isServer || (mFlags & nsMsgFolderFlags::SpecialUse)); return NS_OK; } NS_IMETHODIMP EwsFolder::EmptyTrash(nsIUrlListener* aListener) { nsCOMPtr trashFolder; nsresult rv = GetTrashFolder(getter_AddRefs(trashFolder)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr db; rv = trashFolder->GetMsgDatabase(getter_AddRefs(db)); NS_ENSURE_SUCCESS(rv, rv); // Start by deleting the messages in the folder. nsTArray keys; rv = db->ListAllKeys(keys); NS_ENSURE_SUCCESS(rv, rv); nsTArray> hdrs; rv = MsgGetHeadersFromKeys(db, keys, hdrs); NS_ENSURE_SUCCESS(rv, rv); if (!hdrs.IsEmpty()) { nsCOMPtr copyListener = do_QueryInterface(aListener); rv = trashFolder->DeleteMessages(hdrs, /* msgWindow = */ nullptr, /* deleteStorage = */ true, /* isMove = */ false, copyListener, /* allowUndo = */ false); } NS_ENSURE_SUCCESS(rv, rv); // Then delete any subfolders. nsCOMPtr client; rv = GetEwsClient(getter_AddRefs(client)); NS_ENSURE_SUCCESS(rv, rv); CopyableTArray> subFolders; rv = trashFolder->GetSubFolders(subFolders); NS_ENSURE_SUCCESS(rv, rv); nsTArray subFolderIds(subFolders.Length()); for (const auto f : subFolders) { nsCString ewsId; rv = f->GetStringProperty(kEwsIdProperty, ewsId); NS_ENSURE_SUCCESS(rv, rv); subFolderIds.AppendElement(ewsId); } RefPtr listener = new EwsSimpleListener( [self = RefPtr(this), trashFolder, subFolders]( const nsTArray& ids, bool useLegacyFallback) { for (const auto f : subFolders) { nsresult rv = trashFolder->PropagateDelete(f, true); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; }); // The IMAP version performs some cleanup around here to efficiently delete // all the local contents, but that's a bad idea for EWS because the two // deletes above use async calls to ensure local copies are only deleted on // success. return client->DeleteFolder(listener, subFolderIds); } NS_IMETHODIMP EwsFolder::CompactAll(nsIUrlListener* aListener, nsIMsgWindow* aMsgWindow) { nsresult rv = NS_OK; nsCOMPtr rootFolder; rv = GetRootFolder(getter_AddRefs(rootFolder)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr msgStore; rv = GetMsgStore(getter_AddRefs(msgStore)); NS_ENSURE_SUCCESS(rv, rv); bool storeSupportsCompaction; msgStore->GetSupportsCompaction(&storeSupportsCompaction); nsTArray> folderArray; if (storeSupportsCompaction) { nsTArray> allDescendants; rv = rootFolder->GetDescendants(allDescendants); NS_ENSURE_SUCCESS(rv, rv); int64_t expungedBytes = 0; for (auto folder : allDescendants) { // If folder doesn't currently have a DB, expungedBytes might be out of // whack. Also the compact might do a folder reparse first, which could // change the expungedBytes count (via Expunge flag in // X-Mozilla-Status). bool hasDB; folder->GetDatabaseOpen(&hasDB); expungedBytes = 0; if (folder) rv = folder->GetExpungedBytes(&expungedBytes); NS_ENSURE_SUCCESS(rv, rv); if (!hasDB || expungedBytes > 0) folderArray.AppendElement(folder); } } return AsyncCompactFolders(folderArray, aListener, aMsgWindow); } NS_IMETHODIMP EwsFolder::Compact(nsIUrlListener* aListener, nsIMsgWindow* aMsgWindow) { return AsyncCompactFolders({this}, aListener, aMsgWindow); } nsresult EwsFolder::GetEwsId(nsACString& ewsId) { nsresult rv = GetStringProperty(kEwsIdProperty, ewsId); NS_ENSURE_SUCCESS(rv, rv); if (ewsId.IsEmpty()) { NS_ERROR(nsPrintfCString( "folder %s initialized as EWS folder, but has no EWS ID", URI().get()) .get()); return NS_ERROR_UNEXPECTED; } return NS_OK; } nsresult EwsFolder::GetEwsClient(IEwsClient** ewsClient) { nsCOMPtr server; nsresult rv = GetServer(getter_AddRefs(server)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr ewsServer(do_QueryInterface(server)); return ewsServer->GetEwsClient(ewsClient); } nsresult EwsFolder::GetTrashFolder(nsIMsgFolder** result) { NS_ENSURE_ARG_POINTER(result); nsCOMPtr rootFolder; nsresult rv = GetRootFolder(getter_AddRefs(rootFolder)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr trashFolder; rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Trash, getter_AddRefs(trashFolder)); // `GetFolderWithFlags()` returns NS_OK even if no folder was found, so we // need to check whether it returned it returned a valid folder. if (!trashFolder) { return NS_ERROR_FAILURE; } trashFolder.forget(result); return NS_OK; } nsresult EwsFolder::SyncMessages(nsIMsgWindow* window, nsIUrlListener* urlListener) { // EWS provides us an opaque value which specifies the last version of // upstream messages we received. Provide that to simplify sync. nsCString syncStateToken; nsresult rv = GetStringProperty(SYNC_STATE_PROPERTY, syncStateToken); if (NS_FAILED(rv)) { syncStateToken = EmptyCString(); } nsCOMPtr feedback = nullptr; if (window) { // Format the message we'll show the user while we wait for the remote // operation to complete. RefPtr l10n = intl::Localization::Create({"messenger/activityFeedback.ftl"_ns}, true); auto l10nArgs = dom::Optional(); l10nArgs.Construct(); nsCString folderName; rv = GetLocalizedName(folderName); NS_ENSURE_SUCCESS(rv, rv); auto idArg = l10nArgs.Value().Entries().AppendElement(); idArg->mKey = "folderName"_ns; idArg->mValue.SetValue().SetAsUTF8String().Assign(folderName); ErrorResult error; nsCString message; l10n->FormatValueSync("looking-for-messages-folder"_ns, l10nArgs, message, error); // Show the message in the status bar. rv = window->GetStatusFeedback(getter_AddRefs(feedback)); NS_ENSURE_SUCCESS(rv, rv); // The window might not be attached to an `nsIMsgStatusFeedback`. This // typically happens with new profiles, because the `nsIMsgStatusFeedback` // is only added after the first account is added. Technically this should // also run after the account is added, but we're might be racing against // the `nsIMsgStatusFeedback` being added to the message window, in which // case it might still be null by the time this runs. if (feedback) { rv = feedback->ShowStatusString(NS_ConvertUTF8toUTF16(message)); NS_ENSURE_SUCCESS(rv, rv); rv = feedback->StartMeteors(); NS_ENSURE_SUCCESS(rv, rv); } } // Define the callbacks for the EWS operation. auto onMessageCreated = [self = RefPtr(this)](const nsACString& ewsId, nsIMsgDBHdr** newHdr) { // Check if a header already exists for this EWS ID. `GetHeaderForItem` // returns `NS_ERROR_NOT_AVAILABLE` when no header exists, so we only want // to move forward with creating one in this case. RefPtr existingHeader; nsresult rv = self->GetHdrForEwsId(ewsId, getter_AddRefs(existingHeader)); // If we could retrieve a header for this item, error immediately. if (NS_SUCCEEDED(rv)) { return NS_ERROR_ILLEGAL_VALUE; } // We already know that `rv` is a failure at this point, so we just need to // check it's not the one failure we want. if (rv != NS_ERROR_NOT_AVAILABLE) { return rv; } nsCOMPtr db; rv = self->GetMsgDatabase(getter_AddRefs(db)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr newHeader; rv = db->CreateNewHdr(nsMsgKey_None, getter_AddRefs(newHeader)); NS_ENSURE_SUCCESS(rv, rv); rv = newHeader->SetStringProperty(kEwsIdProperty, ewsId); NS_ENSURE_SUCCESS(rv, rv); newHeader.forget(newHdr); return NS_OK; }; auto onMessageDeleted = [self = RefPtr(this)](const nsACString& ewsId) { // Delete the message headers from the database. nsCOMPtr db; nsresult rv = self->GetMsgDatabase(getter_AddRefs(db)); NS_ENSURE_SUCCESS(rv, rv); RefPtr existingHeader; rv = db->GetMsgHdrForEwsItemID(ewsId, getter_AddRefs(existingHeader)); NS_ENSURE_SUCCESS(rv, rv); if (!existingHeader) { // If we don't have a header for this message ID, it means we have already // deleted it locally. This can happen in legitimate situations, e.g. when // syncing the message list after deleting a message from Thunderbird (in // which case, the server's sync response will include a `Delete` change // for the message we've just deleted). return NS_OK; } return LocalDeleteMessages(self, {existingHeader}); }; auto onMessageUpdated = [self = RefPtr(this)](const nsACString& ewsId, nsIMsgDBHdr** hdr) { RefPtr existingHdr; nsresult rv = self->GetHdrForEwsId(ewsId, getter_AddRefs(existingHdr)); NS_ENSURE_SUCCESS(rv, rv); // The message content might have changed (e.g. if a draft was updated), and // there's no way for us to know for sure without re-downloading it. So // let's delete its content from the message store so it can be // re-downloaded later. uint32_t flags; rv = existingHdr->GetFlags(&flags); NS_ENSURE_SUCCESS(rv, rv); if (!(flags & nsMsgMessageFlags::Offline)) { // Bail early if there's nothing to remove. return NS_OK; } // Delete the message content from the local store. nsCOMPtr store; rv = self->GetMsgStore(getter_AddRefs(store)); NS_ENSURE_SUCCESS(rv, rv); rv = store->DeleteMessages({existingHdr}); NS_ENSURE_SUCCESS(rv, rv); // Update the flags on the database entry to reflect its content is *not* // stored offline anymore. We don't commit right now, but the expectation is // that the consumer will call `CommitChanges()` once it's done processing // the current change. uint32_t unused; rv = existingHdr->AndFlags(~nsMsgMessageFlags::Offline, &unused); NS_ENSURE_SUCCESS(rv, rv); existingHdr.forget(hdr); return NS_OK; }; auto onReadStatusChanged = [self = RefPtr(this)](const nsACString& ewsId, bool is_read) { // Get the header for the message with ewsId and update its read flag in the // database. RefPtr existingHeader; nsresult rv = self->GetHdrForEwsId(ewsId, getter_AddRefs(existingHeader)); NS_ENSURE_SUCCESS(rv, rv); return existingHeader->MarkRead(is_read); }; auto onDetachedHdrPopulated = [self = RefPtr(this)](nsIMsgDBHdr* hdr, nsTArray>& newMessages) { nsCOMPtr db; nsresult rv = self->GetMsgDatabase(getter_AddRefs(db)); NS_ENSURE_SUCCESS(rv, rv); // If New flag is not set, it won't be added to the databases list of // new messages (and so won't be filtered/classified). But we'll treat // read messages as old. uint32_t flags; rv = hdr->GetFlags(&flags); NS_ENSURE_SUCCESS(rv, rv); if (!(flags & nsMsgMessageFlags::Read)) { flags |= nsMsgMessageFlags::New; hdr->SetFlags(flags); newMessages.AppendElement(hdr); } nsCOMPtr liveHdr; rv = db->AttachHdr(hdr, true, getter_AddRefs(liveHdr)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr notifier = mozilla::components::FolderNotification::Service(); // Remember message for filtering at end of sync operation. notifier->NotifyMsgAdded(liveHdr); return NS_OK; }; auto onExistingHdrChanged = [self = RefPtr(this)]() { RefPtr db; nsresult rv = self->GetMsgDatabase(getter_AddRefs(db)); NS_ENSURE_SUCCESS(rv, rv); return db->Commit(nsMsgDBCommitType::kLargeCommit); }; auto onSyncStateTokenChanged = [self = RefPtr(this)](const nsACString& syncStateToken) { return self->SetStringProperty(SYNC_STATE_PROPERTY, syncStateToken); }; // This lambda will be called whenever the sync terminates, regardless of the // outcome. This means it will both be called at the end of `onSyncComplete`, // and used as the listener's `OnOperationFailure` callback. nsCOMPtr syncUrlListener = urlListener; auto onSyncStop = [self = RefPtr(this), syncUrlListener, feedback](nsresult status) { if (syncUrlListener) { nsCOMPtr folderUri; nsresult rv = FolderUri(self, getter_AddRefs(folderUri)); NS_ENSURE_SUCCESS(rv, rv); syncUrlListener->OnStopRunningUrl(folderUri, rv); } if (feedback) { // Reset the status bar. return feedback->StopMeteors(); } return NS_OK; }; auto onSyncComplete = [self = RefPtr(this), onSyncStop]( const nsTArray>& newMessages) { // Trigger notifications for new messages. if (!newMessages.IsEmpty()) { self->SetHasNewMessages(true); self->SetNumNewMessages(static_cast(newMessages.Length())); self->SetBiffState(nsIMsgFolder::nsMsgBiffState_NewMail); // Mark them as requiring filtering. for (nsIMsgDBHdr* msg : newMessages) { nsMsgKey key; msg->GetMessageKey(&key); static_cast(self->mRequireFiltering.put(key)); } // We might be able to apply filtering right away (if the full message // body isn't required). self->PerformFiltering(); // Tell the AutoSyncState about the newly-added messages, // to queue them for potential offline download. { nsTArray keys(newMessages.Length()); for (nsIMsgDBHdr* hdr : newMessages) { nsMsgKey key; hdr->GetMessageKey(&key); MOZ_ASSERT(key != nsMsgKey_None); keys.AppendElement(key); } self->AutoSyncState()->OnNewHeaderFetchCompleted(keys); } } onSyncStop(NS_OK); self->NotifyFolderEvent(kFolderLoaded); return NS_OK; }; RefPtr listener = new EwsMessageSyncListener( onMessageCreated, onMessageDeleted, onMessageUpdated, onDetachedHdrPopulated, onExistingHdrChanged, onSyncStateTokenChanged, onSyncComplete, onReadStatusChanged, onSyncStop); if (urlListener) { nsCOMPtr folderUri; rv = FolderUri(this, getter_AddRefs(folderUri)); NS_ENSURE_SUCCESS(rv, rv); rv = urlListener->OnStartRunningUrl(folderUri); NS_ENSURE_SUCCESS(rv, rv); } // Sync the message list for the current folder. nsCString ewsId; MOZ_TRY(GetEwsId(ewsId)); nsCOMPtr client; MOZ_TRY(GetEwsClient(getter_AddRefs(client))); return client->SyncMessagesForFolder(listener, ewsId, syncStateToken); } nsAutoSyncState* EwsFolder::AutoSyncState() { if (!mAutoSyncState) { // Lazy creation. mAutoSyncState = new nsAutoSyncState(this); } return mAutoSyncState; } NS_IMETHODIMP EwsFolder::GetAutoSyncStateObj( nsIAutoSyncState** autoSyncStateObj) { NS_ENSURE_ARG_POINTER(autoSyncStateObj); NS_IF_ADDREF(*autoSyncStateObj = AutoSyncState()); return NS_OK; } NS_IMETHODIMP EwsFolder::HandleDownloadedMessages() { // There may be filters that were waiting for the full message. PerformFiltering(); return NS_OK; } nsresult EwsFolder::GetHdrForEwsId(const nsACString& ewsId, nsIMsgDBHdr** hdr) { nsCOMPtr db; nsresult rv = GetMsgDatabase(getter_AddRefs(db)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr existingHdr; rv = db->GetMsgHdrForEwsItemID(ewsId, getter_AddRefs(existingHdr)); NS_ENSURE_SUCCESS(rv, rv); // Make sure we managed to get a header from the database. if (!existingHdr) { return NS_ERROR_NOT_AVAILABLE; } existingHdr.forget(hdr); return NS_OK; } // Apply filtering to as many of the mRequireFiltering messages as we can. nsresult EwsFolder::PerformFiltering() { nsresult rv; // Do the filters require full message body? bool incomingFiltersRequireBody; { nsCOMPtr filterList; rv = GetFilterList(nullptr, getter_AddRefs(filterList)); NS_ENSURE_SUCCESS(rv, rv); rv = filterList->DoFiltersNeedMessageBody(nsMsgFilterType::Incoming, &incomingFiltersRequireBody); NS_ENSURE_SUCCESS(rv, rv); } // Collect up the messages we can filter now. nsTArray> targetMsgs(mRequireFiltering.count()); for (auto it = mRequireFiltering.modIter(); !it.done(); it.next()) { nsCOMPtr msg; GetMessageHeader(it.get(), getter_AddRefs(msg)); if (!msg) { // The message could have have been manually deleted or something. it.remove(); continue; } if (incomingFiltersRequireBody) { uint32_t flags; msg->GetFlags(&flags); if (!(flags & nsMsgMessageFlags::Offline)) { // We need the full message, but it's not (yet) available. // Leave for next time. continue; } } targetMsgs.AppendElement(msg); it.remove(); } MOZ_LOG_FMT(FILTERLOGMODULE, LogLevel::Info, "EWS PerformFiltering(): can filter {} messages now, leaving {} " "(incomingFiltersRequireBody={})", targetMsgs.Length(), mRequireFiltering.count(), incomingFiltersRequireBody); if (!targetMsgs.IsEmpty()) { // Once the filtering is complete, `doneFunc` will run. auto doneFunc = [self = RefPtr(this)]( nsresult status, const nsTArray>& msgs) -> nsresult { nsresult rv = self->NotifyFolderEvent(kFiltersApplied); NS_ENSURE_SUCCESS(rv, rv); // Now run the spam classification. // This will invoke OnMessageClassified(). // TODO: // CallFilterPlugins() should take a // nsIJunkMailClassificationListener param instead of relying on // folder inheritance. bool filtersRun; rv = self->CallFilterPlugins(nullptr, &filtersRun); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; }; // Run the filters upon the target messages. Note, by this time, the // messages have already been added to the folders database. // This means we can use ApplyFilters, which handles all the filter // actions - it uses the protocol-agnostic code, as if the filters // had been manually triggered ("run filters now"). This is in // contrast to POP3 and IMAP, which run the filters _before_ adding // the messages to the database, but then have to implement all // their own filter actions. nsCOMPtr filterService( mozilla::components::Filter::Service()); rv = filterService->ApplyFilters( nsMsgFilterType::Inbox, targetMsgs, this, nullptr /*window*/, new MsgOperationListener(targetMsgs, doneFunc)); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } NS_IMETHODIMP EwsFolder::AddSubfolder(const nsACString& folderName, nsIMsgFolder** newFolder) { NS_ENSURE_ARG_POINTER(newFolder); nsresult rv = nsMsgDBFolder::AddSubfolder(folderName, newFolder); NS_ENSURE_SUCCESS(rv, rv); // Check to see if we have a trash folder path saved in prefs. nsCOMPtr server; rv = GetServer(getter_AddRefs(server)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr ewsServer{do_QueryInterface(server, &rv)}; NS_ENSURE_SUCCESS(rv, rv); nsAutoCString trashFolderPath; rv = ewsServer->GetTrashFolderPath(trashFolderPath); NS_ENSURE_SUCCESS(rv, rv); nsAutoCString folderPath; rv = FolderPathInServer(*newFolder, folderPath); NS_ENSURE_SUCCESS(rv, rv); uint32_t flags; rv = (*newFolder)->GetFlags(&flags); NS_ENSURE_SUCCESS(rv, rv); if (trashFolderPath.IsEmpty()) { // If we don't have a trash folder preference value set, and this folder // has the trash flag set, set the preference value to this folder's path. if (flags & nsMsgFolderFlags::Trash) { rv = ewsServer->SetTrashFolderPath(folderPath); NS_ENSURE_SUCCESS(rv, rv); } } else if (trashFolderPath.Equals(folderPath)) { // If the new folder path matches the trash folder path, ensure the trash // folder flag is set for that folder. flags |= nsMsgFolderFlags::Trash; } else { // The trash folder is set and is not equal to this folder. Clear the // trash folder flag. flags &= ~nsMsgFolderFlags::Trash; } // Should this folder download messages for offline? { bool setNewFoldersForOffline = false; rv = server->GetOfflineDownload(&setNewFoldersForOffline); if (NS_SUCCEEDED(rv) && setNewFoldersForOffline) { flags |= nsMsgFolderFlags::Offline; } } rv = (*newFolder)->SetFlags(flags); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } // This callback is invoked for spam handling, by CallFilterPlugins(). // Really, CallFilterPlugins should be altered to take a listener rather than // relying on the folder implementation... but for now, this implementation // is pure cut&paste from nsLocalMailFolder. The IMAP one is similar, but // coalesces server move operations. // // This function is called once per message, then once again with // an empty URI to mark the end of the batch. // It accumulates the messages to move to the junk folder using the // mSpamKeysToMove array, then performs the move at the end of the batch. NS_IMETHODIMP EwsFolder::OnMessageClassified(const nsACString& aMsgURI, nsMsgJunkStatus aClassification, uint32_t aJunkPercent) { nsCOMPtr server; nsresult rv = GetServer(getter_AddRefs(server)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr spamSettings; rv = server->GetSpamSettings(getter_AddRefs(spamSettings)); NS_ENSURE_SUCCESS(rv, rv); nsCString spamFolderURI; rv = spamSettings->GetSpamFolderURI(spamFolderURI); NS_ENSURE_SUCCESS(rv, rv); // Empty URI indicates end of batch. if (!aMsgURI.IsEmpty()) { nsCOMPtr msgHdr; rv = GetMsgDBHdrFromURI(aMsgURI, getter_AddRefs(msgHdr)); NS_ENSURE_SUCCESS(rv, rv); nsMsgKey msgKey; rv = msgHdr->GetMessageKey(&msgKey); NS_ENSURE_SUCCESS(rv, rv); // check if this message needs junk classification uint32_t processingFlags; GetProcessingFlags(msgKey, &processingFlags); if (processingFlags & nsMsgProcessingFlags::ClassifyJunk) { nsMsgDBFolder::OnMessageClassified(aMsgURI, aClassification, aJunkPercent); if (aClassification == nsIJunkMailPlugin::JUNK) { bool willMoveMessage = false; // don't do the move when we are opening up // the junk mail folder or the trash folder // or when manually classifying messages in those folders if (!(mFlags & nsMsgFolderFlags::Junk || mFlags & nsMsgFolderFlags::Trash)) { bool moveOnSpam = false; rv = spamSettings->GetMoveOnSpam(&moveOnSpam); NS_ENSURE_SUCCESS(rv, rv); if (moveOnSpam) { nsCOMPtr folder; rv = FindFolder(spamFolderURI, getter_AddRefs(folder)); NS_ENSURE_SUCCESS(rv, rv); if (folder) { rv = folder->SetFlag(nsMsgFolderFlags::Junk); NS_ENSURE_SUCCESS(rv, rv); mSpamKeysToMove.AppendElement(msgKey); willMoveMessage = true; } else { // XXX TODO // JUNK MAIL RELATED // the listener should do // rv = folder->SetFlag(nsMsgFolderFlags::Junk); // NS_ENSURE_SUCCESS(rv,rv); // mSpamKeysToMove.AppendElement(msgKey); // willMoveMessage = true; rv = GetOrCreateJunkFolder(spamFolderURI, nullptr /* aListener */); NS_ASSERTION(NS_SUCCEEDED(rv), "GetOrCreateJunkFolder failed"); } } } rv = spamSettings->LogJunkHit(msgHdr, willMoveMessage); NS_ENSURE_SUCCESS(rv, rv); } } } else { // URI is empty, indicating end of batch. // Parent will apply post bayes filters. nsMsgDBFolder::OnMessageClassified(EmptyCString(), nsIJunkMailPlugin::UNCLASSIFIED, 0); nsTArray> messages; if (!mSpamKeysToMove.IsEmpty()) { nsCOMPtr folder; if (!spamFolderURI.IsEmpty()) { rv = FindFolder(spamFolderURI, getter_AddRefs(folder)); NS_ENSURE_SUCCESS(rv, rv); } for (uint32_t keyIndex = 0; keyIndex < mSpamKeysToMove.Length(); keyIndex++) { // If an upstream filter moved this message, don't move it here. nsMsgKey msgKey = mSpamKeysToMove.ElementAt(keyIndex); nsMsgProcessingFlagType processingFlags; GetProcessingFlags(msgKey, &processingFlags); if (folder && !(processingFlags & nsMsgProcessingFlags::FilterToMove)) { nsCOMPtr mailHdr; rv = GetMessageHeader(msgKey, getter_AddRefs(mailHdr)); if (NS_SUCCEEDED(rv) && mailHdr) messages.AppendElement(mailHdr); } else { // We don't need the processing flag any more. AndProcessingFlags(msgKey, ~nsMsgProcessingFlags::FilterToMove); } } if (folder) { nsCOMPtr copySvc = mozilla::components::Copy::Service(); rv = copySvc->CopyMessages( this, messages, folder, true, /*nsIMsgCopyServiceListener* listener*/ nullptr, nullptr, false /*allowUndo*/); NS_ASSERTION(NS_SUCCEEDED(rv), "CopyMessages failed"); if (NS_FAILED(rv)) { nsAutoCString logMsg( "failed to copy junk messages to junk folder rv = "); logMsg.AppendInt(static_cast(rv), 16); spamSettings->LogJunkString(logMsg.get()); } } } int32_t numNewMessages; GetNumNewMessages(false, &numNewMessages); SetNumNewMessages(numNewMessages - messages.Length()); mSpamKeysToMove.Clear(); // check if this is the inbox first... if (mFlags & nsMsgFolderFlags::Inbox) PerformBiffNotifications(); } return NS_OK; } NS_IMETHODIMP EwsFolder::HandleViewCommand( int32_t command, const nsTArray& messageKeys, nsIMsgWindow* window, nsIMsgCopyServiceListener* listener) { nsresult rv; if (command == nsMsgViewCommandType::junk || command == nsMsgViewCommandType::unjunk) { const bool isJunk = command == nsMsgViewCommandType::junk; // Get the EWS IDs for the message keys. nsTArray> headers; headers.SetCapacity(messageKeys.Length()); for (auto&& messageKey : messageKeys) { nsCOMPtr header; rv = GetMessageHeader(messageKey, getter_AddRefs(header)); NS_ENSURE_SUCCESS(rv, rv); headers.AppendElement(header); } nsTArray ewsIds; rv = GetEwsIdsForMessageHeaders(headers, ewsIds); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr server; rv = GetServer(getter_AddRefs(server)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr rootFolder; rv = server->GetRootFolder(getter_AddRefs(rootFolder)); NS_ENSURE_SUCCESS(rv, rv); // According to the documentation at // https://learn.microsoft.com/en-us/exchange/client-developer/web-service-reference/markasjunk-operation // If an item is marked as junk, it is moved from the source // folder to the junk folder. If an item is marked as not junk, it // is moved from the source folder to the *inbox*. nsCOMPtr destinationFolder; if (isJunk) { rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Junk, getter_AddRefs(destinationFolder)); NS_ENSURE_SUCCESS(rv, rv); } else { rv = rootFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox, getter_AddRefs(destinationFolder)); NS_ENSURE_SUCCESS(rv, rv); } nsAutoCString legacyDestinationEwsId; if (destinationFolder) { rv = destinationFolder->GetStringProperty(kEwsIdProperty, legacyDestinationEwsId); NS_ENSURE_SUCCESS(rv, rv); } RefPtr operationListener = new EwsSimpleMessageListener{ headers, [self = RefPtr(this), window = RefPtr(window), listener = RefPtr(listener), destinationFolder]( const nsTArray>& headers, const nsTArray& movedItemIds, bool useLegacyFallback) { nsresult rv = NS_OK; auto notifyCopyServiceOnExit = GuardCopyServiceListener(listener, rv); if (useLegacyFallback) { // We didn't get new IDs from the operation, so just trigger // a sync on the destination folder. if (destinationFolder) { // Best we can do is a sync of the supposed destination. rv = destinationFolder->GetNewMessages(window, nullptr); NS_ENSURE_SUCCESS(rv, rv); } // If we're here, it means we've triggered a server-side move. // This means we need to sync the current folder, so we can pick // up the removal of the target message(s). rv = self->SyncMessages(window, nullptr); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } if (movedItemIds.Length() != headers.Length()) { // Make sure the copy service listener is appropriately // notified. rv = NS_ERROR_UNEXPECTED; NS_ENSURE_SUCCESS(rv, rv); } // Copy the input messages to the destination folder. if (destinationFolder) { nsTArray> newHeaders; rv = LocalCopyMessages(self, destinationFolder, headers, newHeaders); NS_ENSURE_SUCCESS(rv, rv); NS_ENSURE_TRUE(newHeaders.Length() == headers.Length(), NS_ERROR_UNEXPECTED); for (auto i = 0u; i < movedItemIds.Length(); ++i) { rv = newHeaders[i]->SetStringProperty(kEwsIdProperty, movedItemIds[i]); NS_ENSURE_SUCCESS(rv, rv); } } // Delete the messages from this folder. rv = LocalDeleteMessages(self, headers); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; }}; nsCOMPtr client; rv = GetEwsClient(getter_AddRefs(client)); NS_ENSURE_SUCCESS(rv, rv); rv = client->MarkItemsAsJunk(operationListener, ewsIds, isJunk, legacyDestinationEwsId); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } NS_IMETHODIMP EwsFolder::FetchMsgPreviewText( nsTArray const& aKeysToFetch, nsIUrlListener* aUrlListener, bool* aAsyncResults) { NS_ENSURE_ARG(aAsyncResults); // We request preview content as part of the initial message sync, so there's // no need for an async request to obtain a preview. *aAsyncResults = false; nsresult rv = NS_OK; for (auto&& key : aKeysToFetch) { nsCOMPtr header; rv = GetMessageHeader(key, getter_AddRefs(header)); NS_ENSURE_SUCCESS(rv, rv); // Check to see if there's already a preview. nsCString previewText; rv = header->GetStringProperty("preview", previewText); if (!previewText.IsEmpty()) { continue; } uint32_t flags; rv = header->GetFlags(&flags); NS_ENSURE_SUCCESS(rv, rv); if (flags & nsMsgMessageFlags::Offline) { nsCOMPtr inputStream; rv = GetMsgInputStream(header, getter_AddRefs(inputStream)); NS_ENSURE_SUCCESS(rv, rv); rv = GetMsgPreviewTextFromStream(header, inputStream); NS_ENSURE_SUCCESS(rv, rv); } } return NS_OK; } NS_IMETHODIMP EwsFolder::ReadFromFolderCacheElem( nsIMsgFolderCacheElement* element) { MOZ_ASSERT(!mail_panorama_enabled_AtStartup()); if (mail_panorama_enabled_AtStartup()) { return NS_ERROR_NOT_IMPLEMENTED; } NS_ENSURE_ARG_POINTER(element); nsresult rv = nsMsgDBFolder::ReadFromFolderCacheElem(element); NS_ENSURE_SUCCESS(rv, rv); if (!UsesLocalizedName()) { return element->GetCachedString("folderName", mName); } return NS_OK; } NS_IMETHODIMP EwsFolder::WriteToFolderCacheElem( nsIMsgFolderCacheElement* element) { MOZ_ASSERT(!mail_panorama_enabled_AtStartup()); if (mail_panorama_enabled_AtStartup()) { return NS_ERROR_NOT_IMPLEMENTED; } NS_ENSURE_ARG_POINTER(element); nsMsgDBFolder::WriteToFolderCacheElem(element); if (!UsesLocalizedName()) { return element->SetCachedString("folderName", mName); } return NS_OK; }