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