From b84383ee5fcb6003c49fc454244c4f97a88a9d37 Mon Sep 17 00:00:00 2001 From: Oliver Wiese <oliver.wiese@fu-berlin.de> Date: Wed, 7 Apr 2021 18:54:41 +0200 Subject: [PATCH] add delete mail --- enzevalos_iphone/MailHandler.swift | 249 ++++++++++++------ enzevalos_iphone/New Group/Mailbot.swift | 1 + enzevalos_iphone/OutgoingMail.swift | 4 +- .../SwiftUI/Inbox/MailListView.swift | 14 +- enzevalos_iphone/mail/IncomingMail.swift | 2 +- .../persistentData/MailRecord.swift | 1 + .../PersistentDataProvider.swift | 34 ++- .../persistentData/Properties.swift | 1 + 8 files changed, 223 insertions(+), 83 deletions(-) diff --git a/enzevalos_iphone/MailHandler.swift b/enzevalos_iphone/MailHandler.swift index ba2ee7a3..53e751a3 100644 --- a/enzevalos_iphone/MailHandler.swift +++ b/enzevalos_iphone/MailHandler.swift @@ -48,6 +48,10 @@ case postcard } + enum FolderError: Error { + case WrongUidValidity + } + class MailHandler { private static let MAXMAILS = 25 private static let extraHeaders = Autocrypt.EXTRAHEADERS @@ -161,7 +165,7 @@ // TODO SEND MAIL... later... func storeIMAP(mail: OutgoingMail, folder: String, callback: ((MailServerConnectionError?) -> Void)?) { // 1. Test if folder exists - let existFolderController = dataProvider.generateFetchedFolderResultsController(folderpath: folder) + let existFolderController = dataProvider.generateFetchedFolderResultsController(folderpath: folder, moc: nil) if existFolderController.fetchedObjects != nil { // 2. Store Mail in test // We can always store encrypted data on the imap server because the user has a key pair and it is users imap account. @@ -274,7 +278,7 @@ guard IMAPSession != nil else { return } - if let f = dataProvider.generateFetchedFolderResultsController(folderpath: folderName).fetchedObjects?.first { + if let f = dataProvider.generateFetchedFolderResultsController(folderpath: folderName, moc: nil).fetchedObjects?.first { let folderstatus = IMAPSession?.folderStatusOperation(folderName) folderstatus?.start {[unowned self] (error, status) -> Void in guard error == nil else { @@ -317,7 +321,7 @@ completionCallback(MailServerConnectionError.NoData) return } - if let folder = dataProvider.generateFetchedFolderResultsController(folderpath: MailHandler.INBOX).fetchedObjects?.first { + if let folder = dataProvider.generateFetchedFolderResultsController(folderpath: MailHandler.INBOX, moc: nil).fetchedObjects?.first { let folderstatus = IMAPSession?.folderStatusOperation(folder.path) folderstatus?.start {[unowned self] (error, status) -> Void in guard error == nil else { @@ -341,7 +345,7 @@ completionCallback(0, completionHandler) return } - if let folder = dataProvider.generateFetchedFolderResultsController(folderpath: MailHandler.INBOX).fetchedObjects?.first { + if let folder = dataProvider.generateFetchedFolderResultsController(folderpath: MailHandler.INBOX, moc: nil).fetchedObjects?.first { let folderstatus = IMAPSession?.folderStatusOperation(folder.path) // Work only in background thread.... var backgroundTaskID: Int? @@ -445,79 +449,176 @@ } } } + + + private func createFolders(of paths: [String], completionHandler: @escaping(_ error: Error?) -> Void) { + guard let session = IMAPSession else { + completionHandler(MailServerConnectionError.ConnectionError) + return + } + session.fetchAllFoldersOperation()?.start({error, folders in + guard error != nil else { + completionHandler(error) + return + } + var myError: Error? = nil + var createFolders = [String]() + if let names = folders?.map({$0.path}) { + for path in paths { + if !names.contains(path) { + createFolders.append(path) + } + } + } + let myGroup = DispatchGroup() + let queue = DispatchQueue(label: "com.letterbox.mailhandler") + queue.async { + for path in createFolders { + myGroup.enter() + session.createFolderOperation(path)?.start({error in + myError = error + myGroup.leave() + }) + } + myGroup.notify(queue: queue, execute: { + completionHandler(myError) + }) + } + }) + } + + private func moveOnServer(uidValidity: UInt32, mails: [UInt64], fromPath: String, toPath: String, completionHandler: @escaping(_ error: Error?) -> Void) { + guard let session = IMAPSession else { + completionHandler(MailServerConnectionError.ConnectionError) + return + } + // 1. Check uid valilidity + session.folderInfoOperation(fromPath)?.start({error, infos in + guard error == nil, let infos = infos else { + completionHandler(error) + return + } + guard infos.uidValidity == uidValidity else { + completionHandler(FolderError.WrongUidValidity) + return + } + // 2. Move mails if uid is valid + let uidSet = MCOIndexSet() + mails.forEach({uidSet.add($0)}) + session.moveMessagesOperation(withFolder: fromPath, uids: uidSet, destFolder: toPath)?.start({(error, _) in + guard error == nil else { + session.copyMessagesOperation(withFolder: fromPath, uids: uidSet, destFolder: toPath)?.start({(error, _) in + completionHandler(error) + }) + return + } + completionHandler(nil) + return + }) + }) + } + + func move(mails: [UInt64], fromPath: String, toPath: String) { + // 1. Check IMAP session + guard let session = IMAPSession else { + return + } + // 2. Check if we have to create a folder. + createFolders(of: [fromPath, toPath], completionHandler: {error in + guard error == nil else { + return + } + // 3. Move mail on server + guard let fromFolder = self.dataProvider.generateFetchedFolderResultsController(folderpath: fromPath, moc: nil).fetchedObjects?.first else { + return + } + if let uidValidity = fromFolder.uidValidity as? UInt32 { + self.moveOnServer(uidValidity: uidValidity, mails: mails, fromPath: fromPath, toPath: toPath, completionHandler: {error in + print("Move mails from \(fromFolder) to \(toPath) complete with error \(error)")}) + // 4. Move mail in core data + self.dataProvider.moveMails(with: mails, from: fromPath, to: toPath) + } + }) + } /* TODO func move(mails: [MailRecord], from: String, to: String, folderCreated: Bool = false) { - guard IMAPSession != nil else { - return - } - let uids = MCOIndexSet() - if !DataHandler.handler.existsFolder(with: to) && !folderCreated { - let op = IMAPSession?.createFolderOperation(to) - op?.start({[unowned self] error in - guard error == nil else { - let conError = MailServerConnectionError.findErrorCode(error: error!) - self.errorhandling(error: conError, originalCall: {self.move(mails: mails, from: from, to: to)}, completionCallback: nil) - return - } - self.move(mails: mails, from: from, to: to, folderCreated: true) - }) - } else { - let folderstatusFrom = IMAPSession?.folderStatusOperation(from) - folderstatusFrom?.start {[unowned self] (error, status) -> Void in - guard error == nil else { - let conerror = MailServerConnectionError.findErrorCode(error: error!) - self.errorhandling(error: conerror, originalCall: {self.move(mails: mails, from: from, to: to)}, completionCallback: nil) - return - } - if let statusFrom = status { - let uidValidity = statusFrom.uidValidity - let f = DataHandler.handler.findFolder(with: from) - if uidValidity == f.uidvalidity { - for mail in mails { - if mail.uidvalidity == uidValidity { - uids.add(mail.uid) - mail.folder.removeFromMails(mail) - if let record = mail.record { - record.removeFromPersistentMails(mail) - if record.mailsInFolder(folder: f).count == 0 { - f.removeFromKeyRecords(record) - } - } - DataHandler.handler.delete(mail: mail) - } - } - let op = self.IMAPSession?.moveMessagesOperation(withFolder: from, uids: uids, destFolder: to) - op?.start {[unowned self] - (err, vanished) -> Void in - guard err == nil else { - let conerror = MailServerConnectionError.findErrorCode(error: err!) - self.errorhandling(error: conerror, originalCall: {self.move(mails: mails, from: from, to: to)}, completionCallback: { err in - guard err != nil else { - return - } - let op = self.IMAPSession?.copyMessagesOperation(withFolder: from, uids: uids, destFolder: to) - op?.start({[unowned self] error, _ in - guard error == nil else { - return - } - uids.enumerate({uid in - self.setFlag(uid, flags: MCOMessageFlag.deleted, folder: from) - }) - }) - }) - return - } - } - } else { - f.uidvalidity = uidValidity - } - } - } - } - } - - */ + guard IMAPSession != nil else { + return + } + let uids = MCOIndexSet() + let provider = PersistentDataProvider.dataProvider + guard let toFolders = provider.generateFetchedFolderResultsController(folderpath: to).fetchedObjects else { + return + } + guard let fromFolder = provider.generateFetchedFolderResultsController(folderpath: from).fetchedObjects?.first else { + return + } + // Create a folder -> Do we have to check if the folder exists on the server? + guard toFolders.isEmpty && !folderCreated else { + let op = IMAPSession?.createFolderOperation(to) + op?.start({[unowned self] error in + guard error == nil else { + let conError = MailServerConnectionError.findErrorCode(error: error!) + self.errorhandling(error: conError, originalCall: {self.move(mails: mails, from: from, to: to)}, completionCallback: nil) + return + } + self.move(mails: mails, from: from, to: to, folderCreated: true) + }) + return + } + let folderstatusFrom = IMAPSession?.folderStatusOperation(from) + folderstatusFrom?.start {[unowned self] (error, status) -> Void in + guard error == nil else { + let conerror = MailServerConnectionError.findErrorCode(error: error!) + self.errorhandling(error: conerror, originalCall: {self.move(mails: mails, from: from, to: to)}, completionCallback: nil) + return + } + if let statusFrom = status, let currentUidValidity = fromFolder.uidValidity as? UInt32 { + let uidValidity = statusFrom.uidValidity + var uids = MCOIndexSet() + if uidValidity == currentUidValidity { + for mail in mails { + uids.add(mail.uID) + mail.folder.removeFromMails(mail) + if let record = mail.record { + record.removeFromPersistentMails(mail) + if record.mailsInFolder(folder: f).count == 0 { + f.removeFromKeyRecords(record) + } + } + DataHandler.handler.delete(mail: mail) + } + let op = self.IMAPSession?.moveMessagesOperation(withFolder: from, uids: uids, destFolder: to) + op?.start {[unowned self] + (err, vanished) -> Void in + guard err == nil else { + let conerror = MailServerConnectionError.findErrorCode(error: err!) + self.errorhandling(error: conerror, originalCall: {self.move(mails: mails, from: from, to: to)}, completionCallback: { err in + guard err != nil else { + return + } + let op = self.IMAPSession?.copyMessagesOperation(withFolder: from, uids: uids, destFolder: to) + op?.start({[unowned self] error, _ in + guard error == nil else { + return + } + uids.enumerate({uid in + self.setFlag(uid, flags: MCOMessageFlag.deleted, folder: from) + }) + }) + }) + return + } + } + } else { + f.uidvalidity = uidValidity + } + } + } + } + */ + func allFolders(_ completion: @escaping (Error?) -> Void) { guard IMAPSession != nil else { completion(MailServerConnectionError.NoData) diff --git a/enzevalos_iphone/New Group/Mailbot.swift b/enzevalos_iphone/New Group/Mailbot.swift index 7ed2e4ef..df9cf6f5 100644 --- a/enzevalos_iphone/New Group/Mailbot.swift +++ b/enzevalos_iphone/New Group/Mailbot.swift @@ -93,6 +93,7 @@ class Mailbot { let mail = MailProperties( messageID: UUID().uuidString, + uid: 0, subject: subject, date: Date(), flags: 0, from: sender, diff --git a/enzevalos_iphone/OutgoingMail.swift b/enzevalos_iphone/OutgoingMail.swift index 5227d034..b6c6482a 100644 --- a/enzevalos_iphone/OutgoingMail.swift +++ b/enzevalos_iphone/OutgoingMail.swift @@ -162,9 +162,9 @@ class OutgoingMail { let to = self.toAddresses.map({AddressProperties(email: $0.mailbox, name: $0.displayName)}) let sigState = self.cryptoObject?.signatureState.rawValue ?? SignatureState.NoSignature.rawValue let encState = self.cryptoObject?.encryptionState.rawValue ?? EncryptionState.NoEncryption.rawValue - + let uuid = UUID() let from = AddressProperties(email: self.sender.mailbox, name: self.sender.displayName) - let m = MailProperties(messageID: UUID().uuidString, subject: subject, date: Date(), flags: 0, from: from, to: to, cc: cc , bcc: bcc, folder: OutgoingMail.OutgoingFolder, body: body, attachments: attachmentProperties, signatureState: sigState, encryptionState: encState, signatureKey: nil, decryptionKey: nil, autocryptHeaderKey: [], attachedPublicKeys: [], attachedSecretKeys: []) + let m = MailProperties(messageID: uuid.uuidString, uid: UInt64(arc4random()), subject: subject, date: Date(), flags: 0, from: from, to: to, cc: cc , bcc: bcc, folder: OutgoingMail.OutgoingFolder, body: body, attachments: attachmentProperties, signatureState: sigState, encryptionState: encState, signatureKey: nil, decryptionKey: nil, autocryptHeaderKey: [], attachedPublicKeys: [], attachedSecretKeys: []) LetterboxModel.instance.dataProvider.importNewData(from: [m], completionHandler: { _ in }) // TODO Fix crypto stuff (keys etc.) } diff --git a/enzevalos_iphone/SwiftUI/Inbox/MailListView.swift b/enzevalos_iphone/SwiftUI/Inbox/MailListView.swift index 6ef156f8..577caca4 100644 --- a/enzevalos_iphone/SwiftUI/Inbox/MailListView.swift +++ b/enzevalos_iphone/SwiftUI/Inbox/MailListView.swift @@ -46,13 +46,21 @@ struct MailListView: View { MailRowView(mail: email) } } - .onDelete {_ in - // TODO: Perform actual deletion of email - } + .onDelete(perform: deleteMails(at:)) } .listStyle(PlainListStyle()) } + private func deleteMails(at offsets: IndexSet) { + var ids = [UInt64]() + for index in offsets { + //PersistentDataProvider.dataProvider.delete(object: mails[index]) + let mail = mails[index] + ids.append(UInt64(mail.uID)) + } + MailHandler.init().move(mails: ids, fromPath: folderPath, toPath: UserManager.backendTrashFolderPath) + } + func filterKeyRecord(keyRecord: MailRecord) -> Bool { if self.searchText.isEmpty || self.searchText == NSLocalizedString("Searchbar.Title", comment: "Search") { diff --git a/enzevalos_iphone/mail/IncomingMail.swift b/enzevalos_iphone/mail/IncomingMail.swift index 93e71adf..e4a41b6a 100644 --- a/enzevalos_iphone/mail/IncomingMail.swift +++ b/enzevalos_iphone/mail/IncomingMail.swift @@ -308,7 +308,7 @@ class IncomingMail { } let f = Int16(self.flags.rawValue) - var m = MailProperties(messageID: self.msgID, subject: subject, date: date, flags: f, from: from, to: to, cc: ccProperties, bcc: bccProperties, folder: folder, body: body, signatureState: sigState, encryptionState: encState, signatureKey: sigKey, decryptionKey: nil, autocryptHeaderKey: autocryptPK, attachedPublicKeys: attPK, attachedSecretKeys: []) + var m = MailProperties(messageID: self.msgID, uid: uID, subject: subject, date: date, flags: f, from: from, to: to, cc: ccProperties, bcc: bccProperties, folder: folder, body: body, signatureState: sigState, encryptionState: encState, signatureKey: sigKey, decryptionKey: nil, autocryptHeaderKey: autocryptPK, attachedPublicKeys: attPK, attachedSecretKeys: []) // TODO: FIX KEYS m.to = to m.cc = ccProperties diff --git a/enzevalos_iphone/persistentData/MailRecord.swift b/enzevalos_iphone/persistentData/MailRecord.swift index 57f9e0b7..46f67548 100644 --- a/enzevalos_iphone/persistentData/MailRecord.swift +++ b/enzevalos_iphone/persistentData/MailRecord.swift @@ -35,6 +35,7 @@ extension MailRecord { flag = mailProperties.flags subject = mailProperties.subject xMailer = mailProperties.xMailer + uID = Int64(mailProperties.uid) // Content diff --git a/enzevalos_iphone/persistentData/PersistentDataProvider.swift b/enzevalos_iphone/persistentData/PersistentDataProvider.swift index 7f2994f2..31664406 100644 --- a/enzevalos_iphone/persistentData/PersistentDataProvider.swift +++ b/enzevalos_iphone/persistentData/PersistentDataProvider.swift @@ -69,7 +69,7 @@ class PersistentDataProvider { var msgs = [MailProperties]() for i in 1...10 { let a = addrs[i%addrs.count] - let msg = MailProperties(messageID: "\(i)", subject: "MSG\(i)", date: Date(), flags: 0, from: a, folder: folder, signatureState: 0, encryptionState: 0) + let msg = MailProperties(messageID: "\(i)", uid: UInt64(i), subject: "MSG\(i)", date: Date(), flags: 0, from: a, folder: folder, signatureState: 0, encryptionState: 0) msgs.append(msg) } return msgs @@ -484,6 +484,23 @@ class PersistentDataProvider { } } + func moveMails(with uids: [UInt64], from: String, to: String) { + guard let rfc = try? lookUp(entityName: MailRecord.entityName, sorting: ["uID": true], equalPredicates: ["inFolder.path": from], differentPredicates: nil, inPredicates: ["uID": uids.map({"\($0)"})]) else { + return + } + guard let toFolder = generateFetchedFolderResultsController(folderpath: to, moc: rfc.managedObjectContext).fetchedObjects?.first else { + importNewData(from: [FolderProperties(delimiter: nil, uidValidity: nil, lastUpdate: nil, maxUID: nil, minUID: nil, path: to, parent: nil, children: nil)], completionHandler: {_ in self.moveMails(with: uids, from: from, to: to)}) + return + } + if let mails = rfc.fetchedObjects as? [MailRecord] { + for mail in mails { + mail.inFolder = toFolder + } + } + try? save(taskContext: rfc.managedObjectContext) + + } + func reset(){ persistentContainer.viewContext.reset() } @@ -677,13 +694,17 @@ class PersistentDataProvider { return generateFetchResultController(entityName: MailRecord.entityName, sortDescriptors: sortDescriptors, predicate: predicate, propertiesToFetch: nil, fetchLimit: PersistentDataProvider.FETCHLIMIT, context: nil) } - func generateFetchedFolderResultsController(folderpath: String) -> NSFetchedResultsController<FolderRecord> { + func generateFetchedFolderResultsController(folderpath: String, moc: NSManagedObjectContext? = nil) -> NSFetchedResultsController<FolderRecord> { + var mymoc = persistentContainer.viewContext + if let moc = moc { + mymoc = moc + } let freq = NSFetchRequest<FolderRecord>(entityName: FolderRecord.entityName) freq.sortDescriptors = [NSSortDescriptor(key: "path", ascending: true)] let predicate = NSPredicate(format: "path == %@", folderpath) freq.predicate = predicate let controller = NSFetchedResultsController(fetchRequest: freq, - managedObjectContext: persistentContainer.viewContext, + managedObjectContext: mymoc , sectionNameKeyPath: nil, cacheName: nil) // Perform the fetch. do { @@ -749,6 +770,13 @@ class PersistentDataProvider { importSecretKeys(secretProperties: [sk], origin: .Generated, completionHandler: completionHandler) return key } + + func delete(object: NSManagedObject) { + if let moc = object.managedObjectContext { + moc.delete(object) + try? save(taskContext: moc) + } + } } diff --git a/enzevalos_iphone/persistentData/Properties.swift b/enzevalos_iphone/persistentData/Properties.swift index 7db6b0d8..c65db191 100644 --- a/enzevalos_iphone/persistentData/Properties.swift +++ b/enzevalos_iphone/persistentData/Properties.swift @@ -22,6 +22,7 @@ struct MailProperties: DataPropertyProtocol { // Header properties let messageID: String + let uid: UInt64 let subject: String let date: Date let flags: Int16 -- GitLab