diff --git a/Mail/Proxy/Implementation/MessageActionHandler.swift b/Mail/Proxy/Implementation/MessageActionHandler.swift index 7654df24f..d85090eae 100644 --- a/Mail/Proxy/Implementation/MessageActionHandler.swift +++ b/Mail/Proxy/Implementation/MessageActionHandler.swift @@ -98,10 +98,7 @@ public struct MessageActionHandler: MessageActionHandlable { /// Silently move mail to a specified folder private func moveMessage(uid: String, to folderRole: FolderRole, mailboxManager: MailboxManager) async throws { - let realm = mailboxManager.getRealm() - realm.refresh() - - guard let notificationMessage = realm.object(ofType: Message.self, forPrimaryKey: uid) else { + guard let notificationMessage = mailboxManager.fetchObject(ofType: Message.self, forPrimaryKey: uid) else { throw ErrorDomain.messageNotFoundInDatabase } diff --git a/Mail/Utils/DraftUtils.swift b/Mail/Utils/DraftUtils.swift index 8eff13de6..a38d887d6 100644 --- a/Mail/Utils/DraftUtils.swift +++ b/Mail/Utils/DraftUtils.swift @@ -31,7 +31,7 @@ enum DraftUtils { ) { guard let message = thread.messages.first else { return } // If we already have the draft locally, present it directly - if let draft = mailboxManager.draft(messageUid: message.uid, using: nil)?.detached() { + if let draft = mailboxManager.draft(messageUid: message.uid)?.detached() { matomoOpenDraft(isLoadedRemotely: false) composeMessageIntent.wrappedValue = ComposeMessageIntent.existing(draft: draft, originMailboxManager: mailboxManager) } else { @@ -45,7 +45,7 @@ enum DraftUtils { composeMessageIntent: Binding ) { // If we already have the draft locally, present it directly - if let draft = mailboxManager.draft(messageUid: message.uid, using: nil)?.detached() { + if let draft = mailboxManager.draft(messageUid: message.uid)?.detached() { matomoOpenDraft(isLoadedRemotely: false) composeMessageIntent.wrappedValue = ComposeMessageIntent.existing(draft: draft, originMailboxManager: mailboxManager) // Draft comes from API, we will update it after showing the ComposeMessageView diff --git a/Mail/Views/AI Writer/AIModel.swift b/Mail/Views/AI Writer/AIModel.swift index 1cca7ccaf..4a8ba1076 100644 --- a/Mail/Views/AI Writer/AIModel.swift +++ b/Mail/Views/AI Writer/AIModel.swift @@ -276,7 +276,7 @@ extension AIModel { extension AIModel { private func getLiveDraft() -> Draft? { - return mailboxManager.getRealm().object(ofType: Draft.self, forPrimaryKey: draft.localUUID) + return mailboxManager.fetchObject(ofType: Draft.self, forPrimaryKey: draft.localUUID) } private func shouldOverrideSubject() -> Bool { diff --git a/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionsView.swift b/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionsView.swift index 64620eec0..7ff47b487 100644 --- a/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionsView.swift +++ b/Mail/Views/Bottom sheets/Actions/ContactActionsView/ContactActionsView.swift @@ -30,7 +30,7 @@ struct ContactActionsView: View { let recipient: Recipient private var actions: [Action] { - let contact = mailboxManager.contactManager.getContact(for: recipient, realm: nil) + let contact = mailboxManager.contactManager.getContact(for: recipient) if contact?.isRemote == true { return [.writeEmailAction, .copyEmailAction] diff --git a/Mail/Views/Menu Drawer/FolderListView.swift b/Mail/Views/Menu Drawer/FolderListView.swift index 574b4215f..f8fe5b9da 100644 --- a/Mail/Views/Menu Drawer/FolderListView.swift +++ b/Mail/Views/Menu Drawer/FolderListView.swift @@ -40,7 +40,7 @@ struct FolderListView: View { UserFoldersListView(folders: viewModel.userFolders) } .onChange(of: mailboxManager) { newMailboxManager in - viewModel.updateFolderListForMailboxManager(newMailboxManager, animateInitialChanges: true) + viewModel.updateFolderListForMailboxManager(transactionable: newMailboxManager, animateInitialChanges: true) } } } diff --git a/Mail/Views/Menu Drawer/FolderListViewModel.swift b/Mail/Views/Menu Drawer/FolderListViewModel.swift index 717ee465e..52e0f7522 100644 --- a/Mail/Views/Menu Drawer/FolderListViewModel.swift +++ b/Mail/Views/Menu Drawer/FolderListViewModel.swift @@ -18,6 +18,7 @@ import Combine import Foundation +import InfomaniakCoreDB import MailCore import RealmSwift import SwiftUI @@ -38,7 +39,7 @@ import SwiftUI init(mailboxManager: MailboxManageable, foldersQuery: @escaping (Query) -> Query = { $0.toolType == nil }) { self.foldersQuery = foldersQuery - updateFolderListForMailboxManager(mailboxManager, animateInitialChanges: false) + updateFolderListForMailboxManager(transactionable: mailboxManager, animateInitialChanges: false) searchQueryObservation = $searchQuery .debounce(for: .milliseconds(150), scheduler: DispatchQueue.main) @@ -49,23 +50,25 @@ import SwiftUI } } - func updateFolderListForMailboxManager(_ mailboxManager: MailboxManageable, animateInitialChanges: Bool) { - foldersObservationToken = mailboxManager.getRealm() - .objects(Folder.self).where(foldersQuery) - .observe(on: DispatchQueue.main) { [weak self] results in - guard let self else { - return - } - - switch results { - case .initial(let folders): - processObservedFolders(folders, animated: animateInitialChanges) - case .update(let folders, _, _, _): - processObservedFolders(folders, animated: true) - case .error: - break - } + func updateFolderListForMailboxManager(transactionable: Transactionable, animateInitialChanges: Bool) { + let objects = transactionable.fetchResults(ofType: Folder.self) { partial in + partial.where(foldersQuery) + } + + foldersObservationToken = objects.observe(on: DispatchQueue.main) { [weak self] results in + guard let self else { + return } + + switch results { + case .initial(let folders): + processObservedFolders(folders, animated: animateInitialChanges) + case .update(let folders, _, _, _): + processObservedFolders(folders, animated: true) + case .error: + break + } + } } private func processObservedFolders(_ folders: Results, animated: Bool) { diff --git a/Mail/Views/New Message/ComposeMessageIntentView.swift b/Mail/Views/New Message/ComposeMessageIntentView.swift index 29dd53936..e0fc5e6d8 100644 --- a/Mail/Views/New Message/ComposeMessageIntentView.swift +++ b/Mail/Views/New Message/ComposeMessageIntentView.swift @@ -16,6 +16,7 @@ along with this program. If not, see . */ +import InfomaniakCoreDB import InfomaniakDI import MailCore import MailCoreUI @@ -98,7 +99,8 @@ struct ComposeMessageIntentView: View, IntentViewable { case .writeTo(let recipient): draftToWrite = Draft.writing(to: recipient) case .reply(let messageUid, let replyMode): - if let frozenMessage = mailboxManager.getRealm().object(ofType: Message.self, forPrimaryKey: messageUid)?.freeze() { + // TODO: Can we move this transaction away from the main actor ? + if let frozenMessage = mailboxManager.fetchObject(ofType: Message.self, forPrimaryKey: messageUid)?.freeze() { let messageReply = MessageReply(frozenMessage: frozenMessage, replyMode: replyMode) maybeMessageReply = messageReply draftToWrite = Draft.replying( @@ -114,7 +116,7 @@ struct ComposeMessageIntentView: View, IntentViewable { if let draftToWrite { let draftLocalUUID = draftToWrite.localUUID - writeDraftToRealm(mailboxManager.getRealm(), draft: draftToWrite) + writeDraftToRealm(mailboxManager, draft: draftToWrite) Task { @MainActor [maybeMessageReply] in guard let liveDraft = mailboxManager.draft(localUuid: draftLocalUUID) else { @@ -135,8 +137,8 @@ struct ComposeMessageIntentView: View, IntentViewable { } } - func writeDraftToRealm(_ realm: Realm, draft: Draft) { - try? realm.write { + func writeDraftToRealm(_ transactionable: Transactionable, draft: Draft) { + try? transactionable.writeTransaction { realm in draft.action = draft.action == nil && draft.remoteUUID.isEmpty ? .initialSave : .save draft.delay = UserDefaults.shared.cancelSendDelay.rawValue diff --git a/Mail/Views/Search/SearchViewModel+Observation.swift b/Mail/Views/Search/SearchViewModel+Observation.swift index 2d166ce54..27450db72 100644 --- a/Mail/Views/Search/SearchViewModel+Observation.swift +++ b/Mail/Views/Search/SearchViewModel+Observation.swift @@ -71,8 +71,9 @@ extension SearchViewModel { let allThreadsUIDs = frozenThreads.map(\.uid) let containAnyOf = NSPredicate(format: Self.containAnyOfUIDs, allThreadsUIDs) - let realm = mailboxManager.getRealm() - let allThreads = realm.objects(Thread.self).filter(containAnyOf) + let allThreads = mailboxManager.fetchResults(ofType: Thread.self) { partial in + partial.filter(containAnyOf) + } observationSearchResultsChangesToken = allThreads.observe(on: observeQueue) { [weak self] changes in guard let self else { diff --git a/Mail/Views/Search/SearchViewModel.swift b/Mail/Views/Search/SearchViewModel.swift index 0b2383c62..686426c10 100644 --- a/Mail/Views/Search/SearchViewModel.swift +++ b/Mail/Views/Search/SearchViewModel.swift @@ -125,7 +125,7 @@ enum SearchState { frozenRealFolder = folder.freezeIfNeeded() frozenSearchFolder = mailboxManager.initSearchFolder().freezeIfNeeded() - frozenFolderList = mailboxManager.getFrozenFolders(using: nil) + frozenFolderList = mailboxManager.getFrozenFolders() searchFieldObservation = $searchValue .debounce(for: .seconds(0.3), scheduler: DispatchQueue.main) diff --git a/Mail/Views/SplitView.swift b/Mail/Views/SplitView.swift index 84173cb28..392af824f 100644 --- a/Mail/Views/SplitView.swift +++ b/Mail/Views/SplitView.swift @@ -238,7 +238,7 @@ struct SplitView: View { try await mailboxManager.refreshAllFolders() let selectedFolderId = mainViewState.selectedFolder.remoteId - guard mailboxManager.getRealm().object(ofType: Folder.self, forPrimaryKey: selectedFolderId) == nil else { + guard mailboxManager.fetchObject(ofType: Folder.self, forPrimaryKey: selectedFolderId) == nil else { return } @@ -306,10 +306,10 @@ struct SplitView: View { try? await Task.sleep(nanoseconds: UInt64(0.5 * Double(NSEC_PER_SEC))) } - let realm = notificationMailboxManager.getRealm() - - let tappedNotificationMessage = realm.object(ofType: Message.self, forPrimaryKey: notificationPayload.messageId)? + let tappedNotificationMessage = notificationMailboxManager.fetchObject(ofType: Message.self, + forPrimaryKey: notificationPayload.messageId)? .freezeIfNeeded() + if notification.name == .onUserTappedNotification { // Original parent should always be in the inbox but maybe change in a later stage to always find the parent in // inbox diff --git a/Mail/Views/Switch User/AccountListView.swift b/Mail/Views/Switch User/AccountListView.swift index 2fd168a24..f7baffce3 100644 --- a/Mail/Views/Switch User/AccountListView.swift +++ b/Mail/Views/Switch User/AccountListView.swift @@ -39,21 +39,22 @@ final class AccountListViewModel: ObservableObject { private var mailboxObservationToken: NotificationToken? init() { - mailboxObservationToken = mailboxInfosManager.getRealm() - .objects(Mailbox.self) - .sorted(by: \.mailboxId) - .observe(on: DispatchQueue.main) { [weak self] results in - switch results { - case .initial(let mailboxes): + let mailboxes = mailboxInfosManager.fetchResults(ofType: Mailbox.self) { partial in + partial.sorted(by: \.mailboxId) + } + + mailboxObservationToken = mailboxes.observe(on: DispatchQueue.main) { [weak self] results in + switch results { + case .initial(let mailboxes): + self?.handleMailboxChanged(Array(mailboxes)) + case .update(let mailboxes, _, _, _): + withAnimation { self?.handleMailboxChanged(Array(mailboxes)) - case .update(let mailboxes, _, _, _): - withAnimation { - self?.handleMailboxChanged(Array(mailboxes)) - } - case .error: - break } + case .error: + break } + } } private func handleMailboxChanged(_ mailboxes: [Mailbox]) { diff --git a/Mail/Views/Thread List/ThreadListViewModel+Observation.swift b/Mail/Views/Thread List/ThreadListViewModel+Observation.swift index 7a0af1a1f..0c2cad77d 100644 --- a/Mail/Views/Thread List/ThreadListViewModel+Observation.swift +++ b/Mail/Views/Thread List/ThreadListViewModel+Observation.swift @@ -133,10 +133,11 @@ extension ThreadListViewModel { } let containAnyOf = NSPredicate(format: Self.containAnyOfUIDs, allThreadsUIDs) - let realm = mailboxManager.getRealm() - let allThreads = realm.objects(Thread.self) - .filter(containAnyOf) - .sorted(by: \.date, ascending: false) + let allThreads = mailboxManager.fetchResults(ofType: Thread.self) { partial in + partial + .filter(containAnyOf) + .sorted(by: \.date, ascending: false) + } observeFilteredThreadsToken = allThreads.observe(on: observeQueue) { [weak self] changes in guard let self else { return } diff --git a/Mail/Views/Thread/Message/MessageView+Preprocessing.swift b/Mail/Views/Thread/Message/MessageView+Preprocessing.swift index ed3f044e9..87bbf3541 100644 --- a/Mail/Views/Thread/Message/MessageView+Preprocessing.swift +++ b/Mail/Views/Thread/Message/MessageView+Preprocessing.swift @@ -72,9 +72,7 @@ final class InlineAttachmentWorker { /// Private accessor on the message private var frozenMessage: Message? { - let realm = mailboxManager.getRealm() - let message = realm.object(ofType: Message.self, forPrimaryKey: messageUid)?.freezeIfNeeded() - return message + mailboxManager.fetchObject(ofType: Message.self, forPrimaryKey: messageUid)?.freezeIfNeeded() } /// A binding on the `PresentableBody` from `MessageView` diff --git a/Mail/Views/Thread/OpenThreadIntentView.swift b/Mail/Views/Thread/OpenThreadIntentView.swift index fd921949a..5a3efb1bc 100644 --- a/Mail/Views/Thread/OpenThreadIntentView.swift +++ b/Mail/Views/Thread/OpenThreadIntentView.swift @@ -72,10 +72,10 @@ struct OpenThreadIntentView: View, IntentViewable { } Task { @MainActor in - let realm = mailboxManager.getRealm() - guard let folder = realm.object(ofType: Folder.self, forPrimaryKey: openThreadIntent.folderId), - let thread = mailboxManager.getRealm().object(ofType: Thread.self, forPrimaryKey: openThreadIntent.threadUid) - else { + guard let folder = mailboxManager.fetchObject(ofType: Folder.self, + forPrimaryKey: openThreadIntent.folderId), + let thread = mailboxManager.fetchObject(ofType: Thread.self, + forPrimaryKey: openThreadIntent.threadUid) else { snackbarPresenter.show(message: MailError.localMessageNotFound.errorDescription ?? "") return } diff --git a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift index 63d92677d..1c505749c 100644 --- a/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift +++ b/MailCore/Cache/Attachments/AttachmentsManagerWorker/AttachmentsManagerWorker.swift @@ -20,6 +20,7 @@ import CocoaLumberjackSwift import Foundation import InfomaniakConcurrency import InfomaniakCore +import InfomaniakCoreDB import UniformTypeIdentifiers /// Abstracts that some attachment was updated @@ -69,6 +70,8 @@ public final class AttachmentsManagerWorker { private let backgroundRealm: BackgroundRealm private let draftLocalUUID: String + public let transactionExecutor: Transactionable + private lazy var filenameDateFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd_HHmmssSS" @@ -88,8 +91,7 @@ public final class AttachmentsManagerWorker { return } - let realm = backgroundRealm.getRealm() - guard let draft = realm.object(ofType: Draft.self, forPrimaryKey: draftLocalUUID), + guard let draft = transactionExecutor.fetchObject(ofType: Draft.self, forPrimaryKey: draftLocalUUID), !draft.isInvalidated, draft.subject.isEmpty else { return @@ -99,8 +101,11 @@ public final class AttachmentsManagerWorker { return } - try? realm.write { - draft.subject = attachmentTitle + try? transactionExecutor.writeTransaction { writableRealm in + guard let liveDraft = writableRealm.object(ofType: Draft.self, forPrimaryKey: draftLocalUUID) else { + return + } + liveDraft.subject = attachmentTitle } } } @@ -109,6 +114,7 @@ public final class AttachmentsManagerWorker { self.backgroundRealm = backgroundRealm self.draftLocalUUID = draftLocalUUID self.mailboxManager = mailboxManager + transactionExecutor = TransactionExecutor(realmAccessible: backgroundRealm) } func addLocalAttachment(attachment: Attachment) async -> Attachment? { @@ -295,7 +301,7 @@ public final class AttachmentsManagerWorker { extension AttachmentsManagerWorker: AttachmentsManagerWorkable { public var liveDraft: Draft? { - guard let liveDraft = backgroundRealm.getRealm().object(ofType: Draft.self, forPrimaryKey: draftLocalUUID), + guard let liveDraft = transactionExecutor.fetchObject(ofType: Draft.self, forPrimaryKey: draftLocalUUID), !liveDraft.isInvalidated else { return nil } diff --git a/MailCore/Cache/ContactManager/ContactManager+DB.swift b/MailCore/Cache/ContactManager/ContactManager+DB.swift index ad5b7a7bd..5a395f3ad 100644 --- a/MailCore/Cache/ContactManager/ContactManager+DB.swift +++ b/MailCore/Cache/ContactManager/ContactManager+DB.swift @@ -17,6 +17,7 @@ */ import Foundation +import InfomaniakCoreDB import RealmSwift public protocol ContactFetchable { @@ -26,7 +27,12 @@ public protocol ContactFetchable { /// - fetchLimit: limit the query by default to limit memory footprint /// - Returns: The collection of matching contacts. func frozenContacts(matching string: String, fetchLimit: Int?) -> any Collection - func getContact(for correspondent: any Correspondent, realm: Realm?) -> MergedContact? + + /// Get a contact from a given transactionable + func getContact(for correspondent: any Correspondent, transactionable: Transactionable) -> MergedContact? + + /// Get a contact from shared contact manager + func getContact(for correspondent: any Correspondent) -> MergedContact? func addressBook(with id: Int) -> AddressBook? func addContact(recipient: Recipient) async throws } @@ -44,9 +50,10 @@ public extension ContactManager { /// - fetchLimit: limit the query by default to limit memory footprint /// - Returns: The collection of matching contacts. Frozen. func frozenContacts(matching string: String, fetchLimit: Int?) -> any Collection { - let realm = getRealm() - let lazyResults = realm - .objects(MergedContact.self) + var lazyResults = fetchResults(ofType: MergedContact.self) { partial in + partial + } + lazyResults = lazyResults .filter(Self.searchContactInsensitivePredicate, string, string) .freeze() @@ -56,20 +63,26 @@ public extension ContactManager { return limitedResults } - func getContact(for correspondent: any Correspondent, realm: Realm? = nil) -> MergedContact? { - let realm = realm ?? getRealm() - let matched = realm.objects(MergedContact.self).where { $0.email == correspondent.email } - return matched.first { $0.name.caseInsensitiveCompare(correspondent.name) == .orderedSame } ?? matched.first + func getContact(for correspondent: any Correspondent) -> MergedContact? { + getContact(for: correspondent, transactionable: self) + } + + func getContact(for correspondent: any Correspondent, transactionable: Transactionable) -> MergedContact? { + transactionable.fetchObject(ofType: MergedContact.self) { partial in + let matched = partial.where { $0.email == correspondent.email } + let result = matched.first { $0.name.caseInsensitiveCompare(correspondent.name) == .orderedSame } ?? matched.first + return result + } } func addressBook(with id: Int) -> AddressBook? { - let realm = getRealm() - return realm.object(ofType: AddressBook.self, forPrimaryKey: id) + fetchObject(ofType: AddressBook.self, forPrimaryKey: id) } private func getDefaultAddressBook() -> AddressBook? { - let realm = getRealm() - return realm.objects(AddressBook.self).where { $0.isDefault == true }.first + fetchObject(ofType: AddressBook.self) { partial in + partial.where { $0.isDefault == true }.first + } } func addContact(recipient: Recipient) async throws { @@ -82,9 +95,8 @@ public extension ContactManager { let mergedContact = MergedContact(email: recipient.email, local: nil, remote: newContact) - let realm = getRealm() - try? realm.safeWrite { - realm.add(mergedContact, update: .modified) + try writeTransaction { writableRealm in + writableRealm.add(mergedContact, update: .modified) } } } diff --git a/MailCore/Cache/ContactManager/ContactManager+Merge.swift b/MailCore/Cache/ContactManager/ContactManager+Merge.swift index f81d95f2f..d457796dd 100644 --- a/MailCore/Cache/ContactManager/ContactManager+Merge.swift +++ b/MailCore/Cache/ContactManager/ContactManager+Merge.swift @@ -155,7 +155,9 @@ extension ContactManager { var idsToDelete = [String]() // enumerate realm contacts - let lazyMergedContacts = getRealm().objects(MergedContact.self) + let lazyMergedContacts = fetchResults(ofType: MergedContact.self) { partial in + partial + } var mergedContactIterator = lazyMergedContacts.makeIterator() while let mergedContact = mergedContactIterator.next() { @@ -181,13 +183,12 @@ extension ContactManager { return } - let cleanupRealm = getRealm() - try? cleanupRealm.safeWrite { + try? writeTransaction { writableRealm in for idToDelete in idsToDelete { - guard let objectToDelete = cleanupRealm.object(ofType: MergedContact.self, forPrimaryKey: idToDelete) else { + guard let objectToDelete = writableRealm.object(ofType: MergedContact.self, forPrimaryKey: idToDelete) else { continue } - cleanupRealm.delete(objectToDelete) + writableRealm.delete(objectToDelete) } } } @@ -198,10 +199,9 @@ extension ContactManager { return } - let realm = getRealm() - try? realm.safeWrite { + try? writeTransaction { writableRealm in for mergedContact in mergedContacts.values { - realm.add(mergedContact, update: .modified) + writableRealm.add(mergedContact, update: .modified) } } } diff --git a/MailCore/Cache/ContactManager/ContactManager+Transactionable.swift b/MailCore/Cache/ContactManager/ContactManager+Transactionable.swift new file mode 100644 index 000000000..d61de071f --- /dev/null +++ b/MailCore/Cache/ContactManager/ContactManager+Transactionable.swift @@ -0,0 +1,22 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2024 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation + +/// Transactionable +extension ContactManager: TransactionablePassthrough {} diff --git a/MailCore/Cache/ContactManager/ContactManager.swift b/MailCore/Cache/ContactManager/ContactManager.swift index 7b8775246..79b8bb9b8 100644 --- a/MailCore/Cache/ContactManager/ContactManager.swift +++ b/MailCore/Cache/ContactManager/ContactManager.swift @@ -20,12 +20,16 @@ import CocoaLumberjackSwift import Contacts import Foundation import InfomaniakCore +import InfomaniakCoreDB import InfomaniakCoreUI import RealmSwift import SwiftRegex /// The composite protocol of the `ContactManager` service -public typealias ContactManageable = ContactFetchable & ContactManagerCoreable & RealmAccessible +public typealias ContactManageable = ContactFetchable + & ContactManagerCoreable + & RealmConfigurable + & Transactionable public protocol ContactManagerCoreable { func refreshContactsAndAddressBooksIfNeeded() async throws @@ -64,11 +68,8 @@ public final class ContactManager: ObservableObject, ContactManageable { public static let constants = ContactManagerConstants() public let realmConfiguration: Realm.Configuration - let backgroundRealm: BackgroundRealm - public lazy var viewRealm: Realm = { - assert(Foundation.Thread.isMainThread, "viewRealm should only be accessed from main thread") - return getRealm() - }() + private let backgroundRealm: BackgroundRealm + public let transactionExecutor: Transactionable let apiFetcher: MailApiFetcher @@ -85,6 +86,7 @@ public final class ContactManager: ObservableObject, ContactManageable { ] ) backgroundRealm = BackgroundRealm(configuration: realmConfiguration) + transactionExecutor = TransactionExecutor(realmAccessible: backgroundRealm) excludeRealmFromBackup() } diff --git a/MailCore/Cache/DraftContentManager.swift b/MailCore/Cache/DraftContentManager.swift index 9ab4cb91a..286ca3a0b 100644 --- a/MailCore/Cache/DraftContentManager.swift +++ b/MailCore/Cache/DraftContentManager.swift @@ -121,8 +121,9 @@ extension DraftContentManager { } private func loadCompleteDraftIfNeeded() async throws -> String { - guard let associatedMessage = mailboxManager.getRealm() - .object(ofType: Message.self, forPrimaryKey: incompleteDraft.messageUid)?.freeze() + guard let associatedMessage = mailboxManager.fetchObject(ofType: Message.self, + forPrimaryKey: incompleteDraft.messageUid)? + .freeze() else { throw MailError.localMessageNotFound } let remoteDraft = try await mailboxManager.apiFetcher.draft(from: associatedMessage) @@ -131,9 +132,9 @@ extension DraftContentManager { remoteDraft.action = .save remoteDraft.delay = incompleteDraft.delay - let realm = mailboxManager.getRealm() - try? realm.safeWrite { - realm.add(remoteDraft.detached(), update: .modified) + let detachedDraft = remoteDraft.detached() + try mailboxManager.writeTransaction { writableRealm in + writableRealm.add(detachedDraft, update: .modified) } return remoteDraft.body @@ -168,9 +169,11 @@ public extension DraftContentManager { } } - let realm = mailboxManager.getRealm() - guard let liveDraft = draft.thaw() else { return } - try? realm.write { + try? mailboxManager.writeTransaction { realm in + guard let liveDraft = realm.object(ofType: Draft.self, forPrimaryKey: draft.localUUID) else { + return + } + if let subject { liveDraft.subject = subject } @@ -185,10 +188,13 @@ public extension DraftContentManager { shouldAddSignatureText: Bool, attachments: [Attachment] ) throws { - let realm = mailboxManager.getRealm() - let liveIncompleteDraft = try getLiveDraft() + var fetchedDraft: Draft? + try mailboxManager.writeTransaction { writableRealm in + guard let liveIncompleteDraft = getLiveDraft(realm: writableRealm) else { + return + } - try? realm.write { + fetchedDraft = liveIncompleteDraft if liveIncompleteDraft.identityId == nil || liveIncompleteDraft.identityId?.isEmpty == true { liveIncompleteDraft.identityId = "\(signature.id)" if shouldAddSignatureText { @@ -203,6 +209,10 @@ public extension DraftContentManager { liveIncompleteDraft.attachments.append(attachment) } } + + guard fetchedDraft != nil else { + throw MailError.unknownError + } } } @@ -317,13 +327,13 @@ extension DraftContentManager { try signatureDiv.html(newSignature.content) } - let realm = mailboxManager.getRealm() - try? realm.write { + try? mailboxManager.writeTransaction { _ in // Keep up to date the rawSignature liveIncompleteDraft.rawSignature = newSignature.content liveIncompleteDraft.identityId = "\(newSignature.id)" liveIncompleteDraft.body = try parsedMessage.outerHtml() } + NotificationCenter.default.post(name: .updateComposeMessageBody, object: nil) } catch { DDLogError("An error occurred while transforming the DOM of the draft: \(error)") @@ -337,7 +347,7 @@ extension DraftContentManager { let defaultSignature = try getDefaultSignature(userSignatures: storedSignatures) // If draft already has an identity, return corresponding signature - if let storedDraft = mailboxManager.getRealm().object(ofType: Draft.self, forPrimaryKey: incompleteDraft.localUUID), + if let storedDraft = mailboxManager.fetchObject(ofType: Draft.self, forPrimaryKey: incompleteDraft.localUUID), let identityId = storedDraft.identityId { return getSignature(for: identityId, userSignatures: storedSignatures) ?? defaultSignature } @@ -448,9 +458,15 @@ extension DraftContentManager { return try await loadReplyingMessage(messageReply.frozenMessage, replyMode: messageReply.replyMode).body?.freezeIfNeeded() } - private func getLiveDraft() throws -> Draft { - let realm = mailboxManager.getRealm() + private func getLiveDraft(realm: Realm) -> Draft? { guard let liveDraft = realm.object(ofType: Draft.self, forPrimaryKey: incompleteDraft.localUUID) else { + return nil + } + return liveDraft + } + + private func getLiveDraft() throws -> Draft { + guard let liveDraft = mailboxManager.fetchObject(ofType: Draft.self, forPrimaryKey: incompleteDraft.localUUID) else { throw MailError.unknownError } return liveDraft diff --git a/MailCore/Cache/DraftManager.swift b/MailCore/Cache/DraftManager.swift index 062175304..41e289df3 100644 --- a/MailCore/Cache/DraftManager.swift +++ b/MailCore/Cache/DraftManager.swift @@ -131,17 +131,16 @@ public final class DraftManager { } var updatedDraft: Draft? - let realm = mailboxManager.getRealm() - try? realm.write { - guard let liveDraft = realm.object(ofType: Draft.self, forPrimaryKey: draft.localUUID) else { + try? mailboxManager.writeTransaction { writableRealm in + guard let liveDraft = writableRealm.object(ofType: Draft.self, forPrimaryKey: draft.localUUID) else { return } - liveDraft.identityId = "\(defaultSignature.id)" - - realm.add(liveDraft, update: .modified) + liveDraft.identityId = "\(defaultSignature.id)" + writableRealm.add(liveDraft, update: .modified) updatedDraft = liveDraft.detached() } + return updatedDraft } @@ -276,12 +275,11 @@ public final class DraftManager { private func deleteEmptyDraft(draft: Draft, for mailboxManager: MailboxManager) { let primaryKey = draft.localUUID - let realm = mailboxManager.getRealm() - try? realm.write { - guard let object = realm.object(ofType: Draft.self, forPrimaryKey: primaryKey) else { + try? mailboxManager.writeTransaction { writableRealm in + guard let object = writableRealm.object(ofType: Draft.self, forPrimaryKey: primaryKey) else { return } - realm.delete(object) + writableRealm.delete(object) } } diff --git a/MailCore/Cache/MailboxInfosManager/MailboxInfosManager+Transactionable.swift b/MailCore/Cache/MailboxInfosManager/MailboxInfosManager+Transactionable.swift new file mode 100644 index 000000000..a484beb8f --- /dev/null +++ b/MailCore/Cache/MailboxInfosManager/MailboxInfosManager+Transactionable.swift @@ -0,0 +1,22 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2024 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation + +/// Transactionable +extension MailboxInfosManager: TransactionablePassthrough {} diff --git a/MailCore/Cache/MailboxInfosManager.swift b/MailCore/Cache/MailboxInfosManager/MailboxInfosManager.swift similarity index 63% rename from MailCore/Cache/MailboxInfosManager.swift rename to MailCore/Cache/MailboxInfosManager/MailboxInfosManager.swift index 71e85bfd1..353fd4801 100644 --- a/MailCore/Cache/MailboxInfosManager.swift +++ b/MailCore/Cache/MailboxInfosManager/MailboxInfosManager.swift @@ -22,16 +22,18 @@ import InfomaniakCoreDB import Realm import RealmSwift -/// Conforming to `RealmAccessible` to get a standard `.getRealm` function -extension MailboxInfosManager: RealmAccessible {} +// So we can exclude it from backups +extension MailboxInfosManager: RealmConfigurable {} public final class MailboxInfosManager { private static let currentDbVersion: UInt64 = 7 - public let realmConfiguration: Realm.Configuration private let dbName = "MailboxInfos.realm" private let backgroundRealm: BackgroundRealm + public let realmConfiguration: Realm.Configuration + public let transactionExecutor: Transactionable + public init() { realmConfiguration = Realm.Configuration( fileURL: MailboxManager.constants.rootDocumentsURL.appendingPathComponent(dbName), @@ -50,6 +52,8 @@ public final class MailboxInfosManager { ) backgroundRealm = BackgroundRealm(configuration: realmConfiguration) + transactionExecutor = TransactionExecutor(realmAccessible: backgroundRealm) + excludeRealmFromBackup() } @@ -59,18 +63,22 @@ public final class MailboxInfosManager { @discardableResult func storeMailboxes(user: InfomaniakCore.UserProfile, mailboxes: [Mailbox]) async -> [Mailbox] { - let realm = getRealm() - for mailbox in mailboxes { - initMailboxForRealm(mailbox: mailbox, userId: user.id) - keepCacheAttributes(for: mailbox, using: realm) - } + var mailboxRemoved = [Mailbox]() + var mailboxRemovedIds = [String]() + try? writeTransaction { writableRealm in + for mailbox in mailboxes { + initMailboxForRealm(mailbox: mailbox, userId: user.id) + keepCacheAttributes(for: mailbox, writableRealm: writableRealm) + } - let mailboxRemoved = getMailboxes(for: user.id, using: realm).filter { currentMailbox in - !mailboxes.contains { newMailbox in - newMailbox.objectId == currentMailbox.objectId + mailboxRemoved = getMailboxes(for: user.id, using: writableRealm).filter { currentMailbox in + !mailboxes.contains { newMailbox in + newMailbox.objectId == currentMailbox.objectId + } } + + mailboxRemovedIds = mailboxRemoved.map(\.objectId) } - let mailboxRemovedIds = mailboxRemoved.map(\.objectId) await backgroundRealm.execute { realm in let detachedMailboxes = mailboxes.map { $0.detached() } @@ -86,22 +94,45 @@ public final class MailboxInfosManager { return "\(mailboxId)_\(userId)" } - public func getMailboxes(for userId: Int? = nil, using realm: Realm? = nil) -> [Mailbox] { - let realm = realm ?? getRealm() + public func getMailboxes(for userId: Int? = nil) -> [Mailbox] { + let mailboxes = fetchResults(ofType: Mailbox.self) { partial in + var realmMailboxList = partial.sorted(by: \Mailbox.mailboxId) + + if let userId { + realmMailboxList = realmMailboxList.where { $0.userId == userId } + } + + let frozenMailboxes = realmMailboxList.freezeIfNeeded() + return frozenMailboxes + } + + return Array(mailboxes) + } + + public func getMailboxes(for userId: Int? = nil, using realm: Realm) -> [Mailbox] { var realmMailboxList = realm.objects(Mailbox.self) .sorted(by: \Mailbox.mailboxId) if let userId { realmMailboxList = realmMailboxList.where { $0.userId == userId } } + return Array(realmMailboxList.map { $0.freeze() }) } - public func getMailbox(id: Int, userId: Int, using realm: Realm? = nil) -> Mailbox? { + public func getMailbox(id: Int, userId: Int) -> Mailbox? { + return getMailbox(objectId: MailboxInfosManager.getObjectId(mailboxId: id, userId: userId)) + } + + public func getMailbox(id: Int, userId: Int, using realm: Realm) -> Mailbox? { return getMailbox(objectId: MailboxInfosManager.getObjectId(mailboxId: id, userId: userId), using: realm) } - public func getMailbox(objectId: String, freeze: Bool = true, using realm: Realm? = nil) -> Mailbox? { - let realm = realm ?? getRealm() + public func getMailbox(objectId: String, freeze: Bool = true) -> Mailbox? { + let mailbox = fetchObject(ofType: Mailbox.self, forPrimaryKey: objectId) + return freeze ? mailbox?.freeze() : mailbox + } + + public func getMailbox(objectId: String, freeze: Bool = true, using realm: Realm) -> Mailbox? { let mailbox = realm.object(ofType: Mailbox.self, forPrimaryKey: objectId) return freeze ? mailbox?.freeze() : mailbox } @@ -115,17 +146,15 @@ public final class MailboxInfosManager { } } - public func keepCacheAttributes(for mailbox: Mailbox, using realm: Realm? = nil) { - let realm = realm ?? getRealm() - guard let savedMailbox = realm.object(ofType: Mailbox.self, forPrimaryKey: mailbox.objectId) else { return } + public func keepCacheAttributes(for mailbox: Mailbox, writableRealm: Realm) { + guard let savedMailbox = writableRealm.object(ofType: Mailbox.self, forPrimaryKey: mailbox.objectId) else { return } mailbox.unseenMessages = savedMailbox.unseenMessages } public func removeMailboxesFor(userId: Int) { - let realm = getRealm() - let userMailboxes = realm.objects(Mailbox.self).where { $0.userId == userId } - try? realm.safeWrite { - realm.delete(userMailboxes) + try? writeTransaction { writableRealm in + let userMailboxes = writableRealm.objects(Mailbox.self).where { $0.userId == userId } + writableRealm.delete(userMailboxes) } } } diff --git a/MailCore/Cache/MailboxManager/MailboxManageable.swift b/MailCore/Cache/MailboxManager/MailboxManageable.swift index 866bb3456..9bd18e526 100644 --- a/MailCore/Cache/MailboxManager/MailboxManageable.swift +++ b/MailCore/Cache/MailboxManager/MailboxManageable.swift @@ -17,6 +17,7 @@ */ import Foundation +import InfomaniakCoreDB import RealmSwift /// An abstract interface on the `MailboxManager` @@ -27,7 +28,8 @@ public typealias MailboxManageable = MailboxManagerCalendareable & MailboxManagerMailboxable & MailboxManagerMessageable & MailboxManagerSearchable - & RealmAccessible + & RealmConfigurable + & Transactionable public protocol MailboxManagerMailboxable { var mailbox: Mailbox { get } @@ -49,9 +51,12 @@ public protocol MailboxManagerMessageable { /// An abstract interface on the `MailboxManager` related to drafts public protocol MailboxManagerDraftable { func draftWithPendingAction() -> Results - func draft(messageUid: String, using realm: Realm?) -> Draft? - func draft(localUuid: String, using realm: Realm?) -> Draft? - func draft(remoteUuid: String, using realm: Realm?) -> Draft? + func draft(messageUid: String) -> Draft? + func draft(messageUid: String, using realm: Realm) -> Draft? + func draft(localUuid: String) -> Draft? + func draft(localUuid: String, using realm: Realm) -> Draft? + func draft(remoteUuid: String) -> Draft? + func draft(remoteUuid: String, using realm: Realm) -> Draft? func send(draft: Draft) async throws -> SendResponse func save(draft: Draft) async throws func delete(draft: Draft) async throws @@ -64,7 +69,7 @@ public protocol MailboxManagerDraftable { public protocol MailboxManagerFolderable { func refreshAllFolders() async throws func getFolder(with role: FolderRole) -> Folder? - func getFrozenFolders(using realm: Realm?) -> [Folder] + func getFrozenFolders() -> [Folder] func createFolder(name: String, parent: Folder?) async throws -> Folder func flushFolder(folder: Folder) async throws -> Bool func refreshFolder(from messages: [Message], additionalFolder: Folder?) async throws diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Calendar.swift b/MailCore/Cache/MailboxManager/MailboxManager+Calendar.swift index eabc0af60..e73aa4d7e 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Calendar.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Calendar.swift @@ -54,7 +54,7 @@ public extension MailboxManager { extension MailboxManager { private func getFrozenMessageAndCalendarAttachment(messageUid: String) throws -> (Message, Attachment) { - guard let frozenMessage = getRealm().object(ofType: Message.self, forPrimaryKey: messageUid)?.freezeIfNeeded(), + guard let frozenMessage = fetchObject(ofType: Message.self, forPrimaryKey: messageUid)?.freezeIfNeeded(), let frozenAttachment = getFrozenCalendarAttachment(from: frozenMessage) else { throw MailError.noCalendarAttachmentFound } diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Draft.swift b/MailCore/Cache/MailboxManager/MailboxManager+Draft.swift index f927fb970..521d7d2f3 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Draft.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Draft.swift @@ -23,22 +23,36 @@ import RealmSwift public extension MailboxManager { func draftWithPendingAction() -> Results { - let realm = getRealm() - return realm.objects(Draft.self).where { $0.action != nil } + fetchResults(ofType: Draft.self) { partial in + partial.where { $0.action != nil } + } + } + + func draft(messageUid: String) -> Draft? { + fetchObject(ofType: Draft.self) { partial in + partial.where { $0.messageUid == messageUid }.first + } } - func draft(messageUid: String, using realm: Realm? = nil) -> Draft? { - let realm = realm ?? getRealm() + func draft(messageUid: String, using realm: Realm) -> Draft? { return realm.objects(Draft.self).where { $0.messageUid == messageUid }.first } - func draft(localUuid: String, using realm: Realm? = nil) -> Draft? { - let realm = realm ?? getRealm() - return realm.objects(Draft.self).where { $0.localUUID == localUuid }.first + func draft(localUuid: String) -> Draft? { + fetchObject(ofType: Draft.self, forPrimaryKey: localUuid) + } + + func draft(localUuid: String, using realm: Realm) -> Draft? { + return realm.object(ofType: Draft.self, forPrimaryKey: localUuid) } - func draft(remoteUuid: String, using realm: Realm? = nil) -> Draft? { - let realm = realm ?? getRealm() + func draft(remoteUuid: String) -> Draft? { + fetchObject(ofType: Draft.self) { partial in + partial.where { $0.remoteUUID == remoteUuid }.first + } + } + + func draft(remoteUuid: String, using realm: Realm) -> Draft? { return realm.objects(Draft.self).where { $0.remoteUUID == remoteUuid }.first } @@ -101,7 +115,14 @@ public extension MailboxManager { throw MailError.resourceError } - if let draft = getRealm().objects(Draft.self).where({ $0.remoteUUID == draftResource }).first?.freeze() { + let draft = fetchObject(ofType: Draft.self) { partial in + partial + .where { $0.remoteUUID == draftResource } + .first? + .freeze() + } + + if let draft { try await deleteLocally(draft: draft) } diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Folders.swift b/MailCore/Cache/MailboxManager/MailboxManager+Folders.swift index 715ad1c23..ba8326283 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Folders.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Folders.swift @@ -77,19 +77,21 @@ public extension MailboxManager { /// - role: Role of the folder. /// - Returns: The folder with the corresponding role, or `nil` if no such folder has been found. func getFolder(with role: FolderRole) -> Folder? { - let realm = getRealm() // Always a new realm, so access is from the correct thread - return realm.objects(Folder.self).where { $0.role == role }.first + fetchObject(ofType: Folder.self) { partial in + partial.where { $0.role == role }.first + } } /// Get all the real folders in Realm - /// - Parameters: - /// - realm: The Realm instance to use. If this parameter is `nil`, a new one will be created. /// - Returns: The list of real folders, frozen. - func getFrozenFolders(using realm: Realm? = nil) -> [Folder] { - let realm = realm ?? getRealm() - let folders = Array(realm.objects(Folder.self).where { $0.toolType == nil }) - let frozenFolders = folders.map { $0.freezeIfNeeded() } - return frozenFolders + func getFrozenFolders() -> [Folder] { + let frozenFolders = fetchResults(ofType: Folder.self) { partial in + partial + .where { $0.toolType == nil } + .freezeIfNeeded() + } + + return Array(frozenFolders) } func createFolder(name: String, parent: Folder?) async throws -> Folder { diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Search.swift b/MailCore/Cache/MailboxManager/MailboxManager+Search.swift index fb492fdaf..217c28984 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Search.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Search.swift @@ -35,10 +35,10 @@ public extension MailboxManager { toolType: .search ) - let realm = getRealm() - try? realm.safeWrite { - realm.add(searchFolder, update: .modified) + try? writeTransaction { writableRealm in + writableRealm.add(searchFolder, update: .modified) } + return searchFolder } diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Signatures.swift b/MailCore/Cache/MailboxManager/MailboxManager+Signatures.swift index 291a8686d..d09e4f92b 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Signatures.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Signatures.swift @@ -32,8 +32,15 @@ public extension MailboxManager { try await refreshAllSignatures() } - func getStoredSignatures(using realm: Realm? = nil) -> [Signature] { - let realm = realm ?? getRealm() + func getStoredSignatures() -> [Signature] { + let signatures = fetchResults(ofType: Signature.self) { partial in + partial + } + + return Array(signatures) + } + + func getStoredSignatures(using realm: Realm) -> [Signature] { return Array(realm.objects(Signature.self)) } } diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Thread.swift b/MailCore/Cache/MailboxManager/MailboxManager+Thread.swift index ab020491a..b4a28d44b 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager+Thread.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager+Thread.swift @@ -63,9 +63,7 @@ public extension MailboxManager { func messages(folder: Folder, isRetrying: Bool = false) async throws { guard !Task.isCancelled else { return } - let realm = getRealm() - let freshFolder = folder.fresh(using: realm) - + let freshFolder = folder.fresh(transactionable: self) let previousCursor = freshFolder?.cursor var messagesUids: MessagesUids @@ -145,7 +143,7 @@ public extension MailboxManager { } if folder.role == .inbox, - let freshFolder = folder.fresh(using: getRealm()) { + let freshFolder = folder.fresh(transactionable: self) { let unreadCount = freshFolder.unreadCount Task { await mailboxInfosManager.updateUnseen(unseenMessages: unreadCount, for: mailbox) @@ -154,8 +152,7 @@ public extension MailboxManager { } } - let realmPrevious = getRealm() - guard let folderPrevious = folder.fresh(using: realmPrevious) else { + guard let folderPrevious = folder.fresh(transactionable: self) else { logError(.missingFolder) return } @@ -189,22 +186,21 @@ public extension MailboxManager { SentryDebug.addBackoffBreadcrumb(folder: folder, index: backoffIndex) - let realm = getRealm() var paginationInfo: PaginationInfo? + let sortedMessages = fetchResults(ofType: Message.self) { partial in + partial.where { $0.folderId == folder.remoteId && !$0.fromSearch } + }.sorted { + guard let firstMessageShortUid = $0.shortUid, + let secondMessageShortUid = $1.shortUid else { + SentryDebug.castToShortUidFailed(firstUid: $0.uid, secondUid: $1.uid) + return false + } - let sortedMessages = realm.objects(Message.self).where { $0.folderId == folder.remoteId && $0.fromSearch == false } - .sorted { - guard let firstMessageShortUid = $0.shortUid, - let secondMessageShortUid = $1.shortUid else { - SentryDebug.castToShortUidFailed(firstUid: $0.uid, secondUid: $1.uid) - return false - } - - if direction == .following { - return firstMessageShortUid > secondMessageShortUid - } - return firstMessageShortUid < secondMessageShortUid + if direction == .following { + return firstMessageShortUid > secondMessageShortUid } + return firstMessageShortUid < secondMessageShortUid + } let backoffOffset = backoffSequence[backoffIndex] - 1 let currentOffset = min(backoffOffset, sortedMessages.count - 1) @@ -296,7 +292,7 @@ public extension MailboxManager { /// - folder: Given folder private func handleMessagesUids(messageUids: MessagesUids, folder: Folder) async throws { let startDate = Date(timeIntervalSinceNow: -5 * 60) - let ignoredIds = folder.fresh(using: getRealm())?.threads + let ignoredIds = folder.fresh(transactionable: self)?.threads .where { $0.date > startDate } .map(\.uid) ?? [] await deleteMessages(uids: messageUids.deletedUids) @@ -305,7 +301,7 @@ public extension MailboxManager { startDate: startDate, folder: folder, alreadyWrongIds: ignoredIds, - realm: getRealm() + transactionable: self ) await updateMessages(updates: messageUids.updated, folder: folder) if !shouldIgnoreNextEvents { @@ -314,7 +310,7 @@ public extension MailboxManager { startDate: startDate, folder: folder, alreadyWrongIds: ignoredIds, - realm: getRealm() + transactionable: self ) } try await addMessages(shortUids: messageUids.addedShortUids, folder: folder, newCursor: messageUids.cursor) @@ -324,7 +320,7 @@ public extension MailboxManager { startDate: startDate, folder: folder, alreadyWrongIds: ignoredIds, - realm: getRealm() + transactionable: self ) } } diff --git a/MailCore/Cache/MailboxManager/MailboxManager+Transactionable.swift b/MailCore/Cache/MailboxManager/MailboxManager+Transactionable.swift new file mode 100644 index 000000000..664ef3c53 --- /dev/null +++ b/MailCore/Cache/MailboxManager/MailboxManager+Transactionable.swift @@ -0,0 +1,22 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2024 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation + +/// Transactionable +extension MailboxManager: TransactionablePassthrough {} diff --git a/MailCore/Cache/MailboxManager/MailboxManager.swift b/MailCore/Cache/MailboxManager/MailboxManager.swift index 115677025..9323c1716 100644 --- a/MailCore/Cache/MailboxManager/MailboxManager.swift +++ b/MailCore/Cache/MailboxManager/MailboxManager.swift @@ -19,6 +19,7 @@ import CocoaLumberjackSwift import Foundation import InfomaniakCore +import InfomaniakCoreDB import InfomaniakDI import RealmSwift import SwiftRegex @@ -33,6 +34,8 @@ public final class MailboxManager: ObservableObject, MailboxManageable { public static let constants = MailboxManagerConstants() public let realmConfiguration: Realm.Configuration + public let transactionExecutor: Transactionable + public let mailbox: Mailbox public let account: Account @@ -111,6 +114,7 @@ public final class MailboxManager: ObservableObject, MailboxManageable { ] ) backgroundRealm = BackgroundRealm(configuration: realmConfiguration) + transactionExecutor = TransactionExecutor(realmAccessible: backgroundRealm) excludeRealmFromBackup() } @@ -150,9 +154,8 @@ public final class MailboxManager: ObservableObject, MailboxManageable { func keepCacheAttributes( for message: Message, keepProperties: MessagePropertiesOptions, - using realm: Realm? = nil + using realm: Realm ) { - let realm = realm ?? getRealm() guard let savedMessage = realm.object(ofType: Message.self, forPrimaryKey: message.uid) else { logError(.missingMessage) return @@ -200,11 +203,6 @@ public final class MailboxManager: ObservableObject, MailboxManageable { } return result } - - public func hasUnreadMessages() -> Bool { - let realm = getRealm() - return realm.objects(Folder.self).contains { $0.unreadCount > 0 } - } } // MARK: - Equatable conformance diff --git a/MailCore/Models/Contact/CommonContact.swift b/MailCore/Models/Contact/CommonContact.swift index b5e800d1f..4a88653a5 100644 --- a/MailCore/Models/Contact/CommonContact.swift +++ b/MailCore/Models/Contact/CommonContact.swift @@ -72,8 +72,8 @@ public final class CommonContact: Identifiable { avatarImageRequest = AvatarImageRequest(imageRequest: nil, shouldAuthenticate: false) } } else { - let mainViewRealm = contextMailboxManager.contactManager.getRealm() - let contact = contextMailboxManager.contactManager.getContact(for: correspondent, realm: mainViewRealm) + let transactionable = contextMailboxManager.contactManager + let contact = contextMailboxManager.contactManager.getContact(for: correspondent, transactionable: transactionable) fullName = contact?.name ?? (correspondent.name.isEmpty ? correspondent.email : correspondent.name) color = UIColor.backgroundColor(from: email.hash, with: UIConstants.avatarColors) avatarImageRequest = AvatarImageRequest(imageRequest: contact?.avatarImageRequest, shouldAuthenticate: true) diff --git a/MailCore/Utils/Model/ModelMigrator.swift b/MailCore/Utils/Model/ModelMigrator.swift index 45344df69..524607e70 100644 --- a/MailCore/Utils/Model/ModelMigrator.swift +++ b/MailCore/Utils/Model/ModelMigrator.swift @@ -30,16 +30,18 @@ public struct ModelMigrator { /// Perform a getRealm on each realm store to trigger a migration if needed public func migrateRealmIfNeeded() { @InjectService var mailboxInfosManager: MailboxInfosManager - // call to .getRealm will trigger the migration if needed - _ = mailboxInfosManager.getRealm() + // .writeTransaction internal call to .getRealm will trigger the migration if needed + try? mailboxInfosManager.writeTransaction { _ in } @InjectService var accountManager: AccountManager if let currentMailboxManager = accountManager.currentMailboxManager { - _ = currentMailboxManager.getRealm() + // Force migration by performing a transaction + try? currentMailboxManager.writeTransaction { _ in } } if let contactManager = accountManager.currentContactManager { - _ = contactManager.getRealm() + // Force migration by performing a transaction + _ = try? contactManager.writeTransaction { _ in } } } } diff --git a/MailCore/Cache/BackgroundRealm.swift b/MailCore/Utils/Model/Realm/BackgroundRealm.swift similarity index 95% rename from MailCore/Cache/BackgroundRealm.swift rename to MailCore/Utils/Model/Realm/BackgroundRealm.swift index 06c4eb543..4a83061e7 100644 --- a/MailCore/Cache/BackgroundRealm.swift +++ b/MailCore/Utils/Model/Realm/BackgroundRealm.swift @@ -21,8 +21,9 @@ import RealmSwift import Sentry /// Conforming to `RealmAccessible` to get a standard `.getRealm` function -extension BackgroundRealm: RealmAccessible {} +extension BackgroundRealm: MailCoreRealmAccessible {} +/// Async await db transactions. Can provide a Realm. public final class BackgroundRealm { private let queue: DispatchQueue diff --git a/MailCore/Utils/Model/Realm/RealmAccessible.swift b/MailCore/Utils/Model/Realm/RealmAccessible.swift index 1534f8103..1e2355545 100644 --- a/MailCore/Utils/Model/Realm/RealmAccessible.swift +++ b/MailCore/Utils/Model/Realm/RealmAccessible.swift @@ -17,25 +17,19 @@ */ import Foundation +import InfomaniakCoreDB import InfomaniakDI import Realm import RealmSwift -/// Something that can access a realm with a given configuration -public protocol RealmAccessible { - /// Configuration for a given realm - var realmConfiguration: Realm.Configuration { get } +/// Centralised way to access a realm configuration and instance. +/// +/// MailCoreRealmAccessible is only intended to be used by `BackgroundRealm` +protocol MailCoreRealmAccessible: RealmAccessible, RealmConfigurable {} - /// Fetches an up to date realm for a given configuration, or fail in a controlled manner - func getRealm() -> Realm - - /// Set `isExcludedFromBackup = true` to the folder where realm is located to exclude a realm cache from an iCloud backup - /// - Important: Avoid calling this method too often as this can be expensive, prefer calling it once at init time - func excludeRealmFromBackup() -} - -public extension RealmAccessible { - func getRealm() -> Realm { +/// Default shared getRealm() implementation with migration retry +extension MailCoreRealmAccessible { + public func getRealm() -> Realm { getRealm(canRetry: true) } @@ -69,7 +63,10 @@ public extension RealmAccessible { return getRealm(canRetry: false) } } +} +/// Default implementation handling iCloud backup exclusion +public extension RealmConfigurable { func excludeRealmFromBackup() { guard var realmFolderURL = realmConfiguration.fileURL?.deletingLastPathComponent() else { return diff --git a/MailCore/Utils/Model/TransactionablePassthrough.swift b/MailCore/Utils/Model/TransactionablePassthrough.swift new file mode 100644 index 000000000..aef7f4337 --- /dev/null +++ b/MailCore/Utils/Model/TransactionablePassthrough.swift @@ -0,0 +1,49 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2024 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Foundation +import InfomaniakCoreDB +import RealmSwift + +/// Something that can use an underlying `transactionExecutor` +/// +/// This wrapping type is intended to reduce boilerplate only +protocol TransactionablePassthrough: Transactionable { + var transactionExecutor: Transactionable { get } +} + +extension TransactionablePassthrough { + public func fetchObject(ofType type: Element.Type, + forPrimaryKey key: KeyType) -> Element? { + return transactionExecutor.fetchObject(ofType: type, forPrimaryKey: key) + } + + public func fetchObject(ofType type: Element.Type, + filtering: (Results) -> Element?) -> Element? { + return transactionExecutor.fetchObject(ofType: type, filtering: filtering) + } + + public func fetchResults(ofType type: Element.Type, + filtering: (Results) -> Results) -> Results { + return transactionExecutor.fetchResults(ofType: type, filtering: filtering) + } + + public func writeTransaction(withRealm realmClosure: (Realm) throws -> Void) throws { + try transactionExecutor.writeTransaction(withRealm: realmClosure) + } +} diff --git a/MailCore/Utils/NotificationsHelper.swift b/MailCore/Utils/NotificationsHelper.swift index c945b1b91..43519ffa5 100644 --- a/MailCore/Utils/NotificationsHelper.swift +++ b/MailCore/Utils/NotificationsHelper.swift @@ -108,7 +108,7 @@ public enum NotificationsHelper { let mailboxManager = accountManager.getMailboxManager(for: mailbox), let messageUid = content.userInfo[NotificationsHelper.UserInfoKeys.messageUid] as? String, !messageUid.isEmpty, - let message = mailboxManager.getRealm().object(ofType: Message.self, forPrimaryKey: messageUid), + let message = mailboxManager.fetchObject(ofType: Message.self, forPrimaryKey: messageUid), message.folder?.role == .inbox, !message.seen else { @@ -203,7 +203,7 @@ public enum NotificationsHelper { mailboxManager: MailboxManager, incompleteNotification: UNMutableNotificationContent ) async -> UNNotificationContent? { - let localContact = mailboxManager.contactManager.getContact(for: fromRecipient, realm: nil) + let localContact = mailboxManager.contactManager.getContact(for: fromRecipient) let handleSender = INPersonHandle(value: fromRecipient.email, type: .emailAddress) let sender = INPerson(personHandle: handleSender, nameComponents: nil, diff --git a/MailCore/Utils/Realm+Extensions.swift b/MailCore/Utils/Realm+Extensions.swift index f6d8370e0..d70aa3aff 100644 --- a/MailCore/Utils/Realm+Extensions.swift +++ b/MailCore/Utils/Realm+Extensions.swift @@ -17,14 +17,25 @@ */ import Foundation +import InfomaniakCoreDB import RealmSwift public extension Object { func fresh(using realm: Realm) -> Self? { - if let primaryKey = objectSchema.primaryKeyProperty?.name, - let primaryKeyValue = value(forKey: primaryKey) { - return realm.object(ofType: Self.self, forPrimaryKey: primaryKeyValue) + guard let primaryKey = objectSchema.primaryKeyProperty?.name, + let primaryKeyValue = value(forKey: primaryKey) else { + return nil } - return nil + + return realm.object(ofType: Self.self, forPrimaryKey: primaryKeyValue) + } + + func fresh(transactionable: Transactionable) -> Self? { + guard let primaryKey = objectSchema.primaryKeyProperty?.name, + let primaryKeyValue = value(forKey: primaryKey) else { + return nil + } + + return transactionable.fetchObject(ofType: Self.self, forPrimaryKey: primaryKeyValue) } } diff --git a/MailCore/Utils/SentryDebug.swift b/MailCore/Utils/SentryDebug.swift index d0999e7ea..a80d0975d 100644 --- a/MailCore/Utils/SentryDebug.swift +++ b/MailCore/Utils/SentryDebug.swift @@ -18,6 +18,7 @@ import Alamofire import Foundation +import InfomaniakCoreDB import InfomaniakLogin import RealmSwift import Sentry @@ -96,8 +97,14 @@ public enum SentryDebug { } } - static func captureWrongDate(step: String, startDate: Date, folder: Folder, alreadyWrongIds: [String], realm: Realm) -> Bool { - guard let freshFolder = folder.fresh(using: realm) else { return false } + static func captureWrongDate( + step: String, + startDate: Date, + folder: Folder, + alreadyWrongIds: [String], + transactionable: Transactionable + ) -> Bool { + guard let freshFolder = folder.fresh(transactionable: transactionable) else { return false } let threads = freshFolder.threads .where { $0.date > startDate } diff --git a/MailNotificationServiceExtension/NotificationService.swift b/MailNotificationServiceExtension/NotificationService.swift index 61f38ccbc..ec160aa9a 100644 --- a/MailNotificationServiceExtension/NotificationService.swift +++ b/MailNotificationServiceExtension/NotificationService.swift @@ -50,7 +50,7 @@ final class NotificationService: UNNotificationServiceExtension { } await mailboxManager.refreshFolderContent(inboxFolder.freezeIfNeeded()) - @ThreadSafe var message = mailboxManager.getRealm().object(ofType: Message.self, forPrimaryKey: uid) + @ThreadSafe var message = mailboxManager.fetchObject(ofType: Message.self, forPrimaryKey: uid) if let message, !message.fullyDownloaded { diff --git a/MailTests/Contacts/UTContactManager.swift b/MailTests/Contacts/UTContactManager.swift index 0c1132ea2..b315de4bb 100644 --- a/MailTests/Contacts/UTContactManager.swift +++ b/MailTests/Contacts/UTContactManager.swift @@ -25,22 +25,23 @@ final class UTContactManager: XCTestCase { override func setUpWithError() throws {} func generateFakeContacts(count: Int) { - let realm = contactManager.getRealm() - // swiftlint:disable:next force_try - try! realm.write { - realm.deleteAll() - } + do { + try contactManager.writeTransaction { writableRealm in + writableRealm.deleteAll() + } - // swiftlint:disable:next force_try - try! realm.write { - for i in 0 ..< count { - let contact = MergedContact() - contact.id = "\(i)" - let randomName = UUID().uuidString - contact.name = "\(randomName)" - contact.email = "\(randomName)@somemail.com" - realm.add(contact) + try contactManager.writeTransaction { writableRealm in + for i in 0 ..< count { + let contact = MergedContact() + contact.id = "\(i)" + let randomName = UUID().uuidString + contact.name = "\(randomName)" + contact.email = "\(randomName)@somemail.com" + writableRealm.add(contact) + } } + } catch { + fatalError("failed transaction in base, error:\(error)") } } diff --git a/MailTests/Folders/ITFolderListViewModel.swift b/MailTests/Folders/ITFolderListViewModel.swift index 22dabfaf2..cdd3b8451 100644 --- a/MailTests/Folders/ITFolderListViewModel.swift +++ b/MailTests/Folders/ITFolderListViewModel.swift @@ -20,15 +20,28 @@ import Combine import Foundation @testable import Infomaniak_Mail import InfomaniakCore +import InfomaniakCoreDB import InfomaniakLogin @testable import MailCore @testable import RealmSwift import XCTest -struct MCKContactManageable_FolderListViewModel: ContactManageable { +struct MCKContactManageable_FolderListViewModel: ContactManageable, MCKTransactionablePassthrough { + var transactionExecutor: Transactionable! + + var realmConfiguration: RealmSwift.Realm.Configuration + + init(realmConfiguration: RealmSwift.Realm.Configuration) { + self.realmConfiguration = realmConfiguration + let backgroundRealm = BackgroundRealm(configuration: realmConfiguration) + transactionExecutor = TransactionExecutor(realmAccessible: backgroundRealm) + } + func frozenContacts(matching string: String, fetchLimit: Int?) -> any Collection { [] } - func getContact(for correspondent: any MailCore.Correspondent, realm: RealmSwift.Realm?) -> MailCore.MergedContact? { nil } + func getContact(for correspondent: any MailCore.Correspondent) -> MailCore.MergedContact? { nil } + + func getContact(for correspondent: any Correspondent, transactionable: Transactionable) -> MergedContact? { nil } func addressBook(with id: Int) -> MailCore.AddressBook? { nil } @@ -39,12 +52,10 @@ struct MCKContactManageable_FolderListViewModel: ContactManageable { func refreshContactsAndAddressBooks() async throws {} static func deleteUserContacts(userId: Int) {} - - var realmConfiguration: RealmSwift.Realm.Configuration } /// A MailboxManageable used to test the FolderListViewModel -struct MCKMailboxManageable_FolderListViewModel: MailboxManageable { +struct MCKMailboxManageable_FolderListViewModel: MailboxManageable, MCKTransactionablePassthrough, RealmAccessible { let mailbox = Mailbox() var contactManager: MailCore.ContactManageable { @@ -110,8 +121,14 @@ struct MCKMailboxManageable_FolderListViewModel: MailboxManageable { func addToSearchHistory(value: String) async {} let realm: Realm + var transactionExecutor: Transactionable! init(realm: Realm) { self.realm = realm + transactionExecutor = TransactionExecutor(realmAccessible: self) + } + + func getRealm() -> Realm { + realm } func draftWithPendingAction() -> RealmSwift.Results { @@ -176,9 +193,19 @@ struct MCKMailboxManageable_FolderListViewModel: MailboxManageable { realm.configuration } - func getRealm() -> Realm { - realm - } + func draft(messageUid: String) -> MailCore.Draft? { nil } + + func draft(messageUid: String, using realm: RealmSwift.Realm) -> MailCore.Draft? { nil } + + func draft(localUuid: String) -> MailCore.Draft? { nil } + + func draft(localUuid: String, using realm: RealmSwift.Realm) -> MailCore.Draft? { nil } + + func draft(remoteUuid: String) -> MailCore.Draft? { nil } + + func draft(remoteUuid: String, using realm: RealmSwift.Realm) -> MailCore.Draft? { nil } + + func getFrozenFolders() -> [MailCore.Folder] { [] } } /// Integration tests of the FolderListViewModel diff --git a/MailTests/Helpers/MCKTransactionablePassthrough.swift b/MailTests/Helpers/MCKTransactionablePassthrough.swift new file mode 100644 index 000000000..9c85b319f --- /dev/null +++ b/MailTests/Helpers/MCKTransactionablePassthrough.swift @@ -0,0 +1,53 @@ +/* + Infomaniak Mail - iOS App + Copyright (C) 2024 Infomaniak Network SA + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +import Combine +import Foundation +@testable import Infomaniak_Mail +import InfomaniakCore +import InfomaniakCoreDB +import InfomaniakLogin +@testable import MailCore +@testable import RealmSwift +import XCTest + +/// Something that can use an underlying forced unwrapped `transactionExecutor` for testing only +protocol MCKTransactionablePassthrough: Transactionable { + var transactionExecutor: Transactionable! { get } +} + +extension MCKTransactionablePassthrough { + public func fetchObject(ofType type: Element.Type, + forPrimaryKey key: KeyType) -> Element? { + return transactionExecutor.fetchObject(ofType: type, forPrimaryKey: key) + } + + public func fetchObject(ofType type: Element.Type, + filtering: (Results) -> Element?) -> Element? { + return transactionExecutor.fetchObject(ofType: type, filtering: filtering) + } + + public func fetchResults(ofType type: Element.Type, + filtering: (Results) -> Results) -> Results { + return transactionExecutor.fetchResults(ofType: type, filtering: filtering) + } + + public func writeTransaction(withRealm realmClosure: (Realm) throws -> Void) throws { + try transactionExecutor.writeTransaction(withRealm: realmClosure) + } +} diff --git a/MailTests/Search/ITSearchViewModel.swift b/MailTests/Search/ITSearchViewModel.swift index 83bef488d..c5df910b1 100644 --- a/MailTests/Search/ITSearchViewModel.swift +++ b/MailTests/Search/ITSearchViewModel.swift @@ -19,6 +19,7 @@ import Combine @testable import Infomaniak_Mail import InfomaniakCore +import InfomaniakCoreDB import InfomaniakLogin @testable import MailCore import RealmSwift @@ -27,10 +28,22 @@ import XCTest // MARK: - Mocking /// A ContactManageable used to test the SearchViewModel -struct MCKContactManageable_SearchViewModel: ContactManageable { +struct MCKContactManageable_SearchViewModel: ContactManageable, MCKTransactionablePassthrough { + var transactionExecutor: Transactionable! + + var realmConfiguration: RealmSwift.Realm.Configuration + + init(realmConfiguration: RealmSwift.Realm.Configuration) { + self.realmConfiguration = realmConfiguration + let backgroundRealm = BackgroundRealm(configuration: realmConfiguration) + transactionExecutor = TransactionExecutor(realmAccessible: backgroundRealm) + } + func frozenContacts(matching string: String, fetchLimit: Int?) -> any Collection { [] } - func getContact(for correspondent: any MailCore.Correspondent, realm: RealmSwift.Realm?) -> MailCore.MergedContact? { nil } + func getContact(for correspondent: any MailCore.Correspondent) -> MailCore.MergedContact? { nil } + + func getContact(for correspondent: any Correspondent, transactionable: Transactionable) -> MergedContact? { nil } func addressBook(with id: Int) -> MailCore.AddressBook? { nil } @@ -41,12 +54,11 @@ struct MCKContactManageable_SearchViewModel: ContactManageable { func refreshContactsAndAddressBooks() async throws {} static func deleteUserContacts(userId: Int) {} - - var realmConfiguration: RealmSwift.Realm.Configuration } /// A MailboxManageable used to test the SearchViewModel -final class MCKMailboxManageable_SearchViewModel: MailboxManageable { +final class MCKMailboxManageable_SearchViewModel: MailboxManageable, MCKTransactionablePassthrough, RealmAccessible { + var transactionExecutor: Transactionable! let mailbox = Mailbox() let targetFolder: Folder let realm: Realm @@ -55,6 +67,7 @@ final class MCKMailboxManageable_SearchViewModel: MailboxManageable { self.realm = realm self.targetFolder = targetFolder self.folderGenerator = folderGenerator + transactionExecutor = TransactionExecutor(realmAccessible: self) } var contactManager: MailCore.ContactManageable { @@ -203,6 +216,20 @@ final class MCKMailboxManageable_SearchViewModel: MailboxManageable { func getRealm() -> Realm { realm } + + func draft(messageUid: String) -> MailCore.Draft? { nil } + + func draft(messageUid: String, using realm: RealmSwift.Realm) -> MailCore.Draft? { nil } + + func draft(localUuid: String) -> MailCore.Draft? { nil } + + func draft(localUuid: String, using realm: RealmSwift.Realm) -> MailCore.Draft? { nil } + + func draft(remoteUuid: String) -> MailCore.Draft? { nil } + + func draft(remoteUuid: String, using realm: RealmSwift.Realm) -> MailCore.Draft? { nil } + + func getFrozenFolders() -> [MailCore.Folder] { [] } } // MARK: - ITSearchViewModel