diff --git a/enzevalos_iphone.xcodeproj/project.pbxproj b/enzevalos_iphone.xcodeproj/project.pbxproj index eab0b94251b234be77ea28f9dc60b95a473aeefa..fca805fdf63edbb8af9abde7028b1ba142a80dba 100644 --- a/enzevalos_iphone.xcodeproj/project.pbxproj +++ b/enzevalos_iphone.xcodeproj/project.pbxproj @@ -69,6 +69,8 @@ 472F39901E252470009260FB /* CNMailAddressesExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472F398F1E252470009260FB /* CNMailAddressesExtension.swift */; }; 4733B1CE25262CDB00AB5600 /* PersistentDataProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4733B1CD25262CDB00AB5600 /* PersistentDataProtocol.swift */; }; 4733B1E52527196100AB5600 /* PersistentDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4733B1E42527196100AB5600 /* PersistentDataProvider.swift */; }; + 4733B202252B142C00AB5600 /* Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4733B201252B142B00AB5600 /* Properties.swift */; }; + 4733B206252B16D100AB5600 /* FolderRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4733B205252B16D100AB5600 /* FolderRecord.swift */; }; 47358D92244A5AEA000116D7 /* SelectableTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47358D91244A5AEA000116D7 /* SelectableTextView.swift */; }; 474054982244D7A9007CF83B /* MailServerConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474054972244D7A9007CF83B /* MailServerConfigurationTest.swift */; }; 474994022261E4E6000F8DA5 /* SimpleSendIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474994012261E4E6000F8DA5 /* SimpleSendIcon.swift */; }; @@ -414,6 +416,8 @@ 472F398F1E252470009260FB /* CNMailAddressesExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CNMailAddressesExtension.swift; sourceTree = "<group>"; }; 4733B1CD25262CDB00AB5600 /* PersistentDataProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentDataProtocol.swift; sourceTree = "<group>"; }; 4733B1E42527196100AB5600 /* PersistentDataProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistentDataProvider.swift; sourceTree = "<group>"; }; + 4733B201252B142B00AB5600 /* Properties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Properties.swift; sourceTree = "<group>"; }; + 4733B205252B16D100AB5600 /* FolderRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderRecord.swift; sourceTree = "<group>"; }; 47358D91244A5AEA000116D7 /* SelectableTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTextView.swift; sourceTree = "<group>"; }; 474054972244D7A9007CF83B /* MailServerConfigurationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailServerConfigurationTest.swift; sourceTree = "<group>"; }; 474994012261E4E6000F8DA5 /* SimpleSendIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleSendIcon.swift; sourceTree = "<group>"; }; @@ -1390,6 +1394,8 @@ 47FAE3482524FB58005A1BCB /* AddressRecord.swift */, 4733B1CD25262CDB00AB5600 /* PersistentDataProtocol.swift */, 4733B1E42527196100AB5600 /* PersistentDataProvider.swift */, + 4733B201252B142B00AB5600 /* Properties.swift */, + 4733B205252B16D100AB5600 /* FolderRecord.swift */, ); path = persistentData; sourceTree = "<group>"; @@ -2404,6 +2410,7 @@ A1EB05841D956867008659C1 /* TableViewDataDelegate.swift in Sources */, 8428A85E1F436A05007649A5 /* CircleView.swift in Sources */, A182182C21E5072200918A29 /* IntroDescriptionViewController.swift in Sources */, + 4733B202252B142C00AB5600 /* Properties.swift in Sources */, 47FAE3492524FB58005A1BCB /* AddressRecord.swift in Sources */, 4775D7AA243F0E260052F2CC /* SimulatorData.swift in Sources */, F1C7AC821FED6473007629DB /* AboutViewController.swift in Sources */, @@ -2449,6 +2456,7 @@ F12060821DA552FC00F6EF37 /* MailHandlerDelegator.swift in Sources */, 47C822622438A81C005BCE73 /* MapView.swift in Sources */, 474994022261E4E6000F8DA5 /* SimpleSendIcon.swift in Sources */, + 4733B206252B16D100AB5600 /* FolderRecord.swift in Sources */, 0ECEA0E8240E7081007DC71E /* SearchHelper.swift in Sources */, A12F91D821F3A99800AB0589 /* NSLayoutConstraintExtension.swift in Sources */, 71DFE5BA240679E80042019C /* HeaderExtractionValues.swift in Sources */, diff --git a/enzevalos_iphone/mail/IncomingMail.swift b/enzevalos_iphone/mail/IncomingMail.swift index 9d02442ff0fac3eccdbb5cfa4c6d3f6da6a82b75..f3ce9a4805b41c30a51dd4a46ce8ccbb970affc8 100644 --- a/enzevalos_iphone/mail/IncomingMail.swift +++ b/enzevalos_iphone/mail/IncomingMail.swift @@ -139,6 +139,7 @@ class IncomingMail { private var rec: [MCOAddress] = [] private var cc: [MCOAddress] = [] + private var bcc: [MCOAddress] = [] private var from: MCOAddress? private var fromKeyIds: [String] { get{ @@ -264,6 +265,23 @@ class IncomingMail { } } + func export() -> MailProperties { + let to = rec.map{$0.export()} + let ccProperties = cc.map{$0.export()} + let bccProperties = bcc.map{$0.export()} + + let folder = FolderProperties(delimiter: nil, icon: nil, uidValidity: nil, lastUpdate: nil, maxUID: nil, minUID: nil, path: self.folderPath) + let from = self.from?.export() ?? nil + let sigState = self.cryptoObj?.signatureState.rawValue ?? 0 + let encState = self.cryptoObj?.encryptionState.rawValue ?? 0 + let f = Int16(self.flags.rawValue) + var m = MailProperties(messageID: self.msgID, subject: subject, date: date, flags: f, from: from, folder: folder, body: body, signatureState: sigState, encryptionState: encState) + m.to = to + m.cc = ccProperties + m.bcc = bccProperties + return m + } + func store(keyRecord: KeyRecord?) -> PersistentMail? { let sk = secretKeys.first //TODO FIX: may import more secret keys? let mail = DataHandler.handler.createMail(uID, sender: from, receivers: rec, cc: cc, time: date, received: true, subject: subject, body: body, readableAttachments: readableAttachments, flags: flags, record: keyRecord, autocrypt: autocrypt, decryptedData: cryptoObj, folderPath: folderPath, secretKey: sk, references: references, mailagent: userAgent, messageID: msgID, encryptedBody: encryptedBody, storeEncrypted: storeEncrypted, attachedSignature: attachedSignature) @@ -642,6 +660,11 @@ class IncomingMail { cc.append(r as! MCOAddress) } } + if let bccs = header.bcc { + for a in bccs { + bcc.append(a as! MCOAddress) + } + } if let _ = header.extraHeaderValue(forName: Autocrypt.AUTOCRYPTHEADER){ let autocrypt = Autocrypt(header: header) diff --git a/enzevalos_iphone/persistentData/AddressRecord.swift b/enzevalos_iphone/persistentData/AddressRecord.swift index 377943683f96db7eb6e4e11b81e19ea510c558bb..59563398d50e56af531af5832e17f6fdbfb59c6e 100644 --- a/enzevalos_iphone/persistentData/AddressRecord.swift +++ b/enzevalos_iphone/persistentData/AddressRecord.swift @@ -6,31 +6,13 @@ // Copyright © 2020 fu-berlin. All rights reserved. // -extension AddressRecord: PersistentDataProtocol { - typealias T = AddressProperties +extension AddressRecord { static var entityName = "AddressRecord" func update(with addressProperties: AddressProperties){ email = addressProperties.email - } -} - -struct AddressProperties: DataPropertyProtocol { - var entityName = "AddressRecord" - - func update(m: Any) -> Bool { - if let m = m as? AddressRecord { - m.update(with: self) - return true + if let name = addressProperties.name { + self.displayname = name } - return false - } - - let email: String - - var dictionary: [String: Any] { - var dic = [String: Any]() - dic["email"] = email - return dic } } diff --git a/enzevalos_iphone/persistentData/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents b/enzevalos_iphone/persistentData/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents index 560e2d56be0729fe2398a2f128e2045b557d4d7c..fee19b7bf690827d93a497419cd31e0db24e6974 100644 --- a/enzevalos_iphone/persistentData/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents +++ b/enzevalos_iphone/persistentData/DataModel.xcdatamodeld/DataModel.xcdatamodel/contents @@ -5,27 +5,63 @@ <attribute name="id" attributeType="String"/> </entity> <entity name="AddressRecord" representedClassName="AddressRecord" syncable="YES" codeGenerationType="class"> + <attribute name="displayname" optional="YES" attributeType="String"/> <attribute name="email" attributeType="String"/> - <relationship name="inFromField" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MailRecord" inverseName="fromAddress" inverseEntity="MailRecord"/> + <relationship name="inBCCField" toMany="YES" deletionRule="Nullify" destinationEntity="MailRecord" inverseName="bccAddresses" inverseEntity="MailRecord"/> + <relationship name="inCCField" toMany="YES" deletionRule="Nullify" destinationEntity="MailRecord" inverseName="ccAddresses" inverseEntity="MailRecord"/> + <relationship name="inFromField" toMany="YES" deletionRule="Nullify" destinationEntity="MailRecord" inverseName="fromAddress" inverseEntity="MailRecord"/> + <relationship name="inToField" toMany="YES" deletionRule="Nullify" destinationEntity="MailRecord" inverseName="toAddresses" inverseEntity="MailRecord"/> <uniquenessConstraints> <uniquenessConstraint> <constraint value="email"/> </uniquenessConstraint> </uniquenessConstraints> </entity> + <entity name="AttachmentRecord" representedClassName="AttachmentRecord" syncable="YES" codeGenerationType="class"> + <attribute name="contentID" optional="YES" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/> + <attribute name="data" optional="YES" attributeType="Binary"/> + <attribute name="mimeType" optional="YES" attributeType="String"/> + <attribute name="name" optional="YES" attributeType="String"/> + <attribute name="type" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> + <relationship name="isPartOfMail" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MailRecord" inverseName="includedAttachments" inverseEntity="MailRecord"/> + </entity> + <entity name="FolderRecord" representedClassName="FolderRecord" syncable="YES" codeGenerationType="class"> + <attribute name="delimiter" optional="YES" attributeType="String"/> + <attribute name="icon" optional="YES" attributeType="String"/> + <attribute name="lastUpdate" optional="YES" attributeType="Date" usesScalarValueType="NO"/> + <attribute name="maxUID" optional="YES" attributeType="Decimal" defaultValueString="0.0"/> + <attribute name="minUID" optional="YES" attributeType="Decimal" defaultValueString="0.0"/> + <attribute name="path" attributeType="String"/> + <attribute name="uidValidity" optional="YES" attributeType="Decimal" defaultValueString="0.0"/> + <relationship name="childrenFolder" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="FolderRecord" inverseName="partentFolder" inverseEntity="FolderRecord"/> + <relationship name="mailsInFolder" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MailRecord" inverseName="inFolder" inverseEntity="MailRecord"/> + <relationship name="partentFolder" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FolderRecord" inverseName="childrenFolder" inverseEntity="FolderRecord"/> + <uniquenessConstraints> + <uniquenessConstraint> + <constraint value="path"/> + </uniquenessConstraint> + </uniquenessConstraints> + </entity> <entity name="MailRecord" representedClassName="MailRecord" syncable="YES" codeGenerationType="class"> <attribute name="body" attributeType="String"/> <attribute name="date" attributeType="Date" usesScalarValueType="NO"/> - <attribute name="encryptionState" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> + <attribute name="encryptionStateInt" attributeType="Integer 16" minValueString="0" maxValueString="3" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="flag" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="messageID" attributeType="String"/> - <attribute name="signatureState" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> + <attribute name="signatureStateInt" attributeType="Integer 16" minValueString="-1" maxValueString="2" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="subject" attributeType="String"/> <attribute name="xMailer" optional="YES" attributeType="String"/> - <relationship name="bccAddresses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="AddressRecord"/> - <relationship name="ccAddresses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="AddressRecord"/> + <relationship name="attachedAutocryptPublicKey" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PublicKeyRecord" inverseName="sendWithAutocryptHeader" inverseEntity="PublicKeyRecord"/> + <relationship name="attachedPublicKeys" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PublicKeyRecord" inverseName="sendWithMail" inverseEntity="PublicKeyRecord"/> + <relationship name="attachedSecretKey" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SecretKeyRecord" inverseName="sendWithMail" inverseEntity="SecretKeyRecord"/> + <relationship name="bccAddresses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="AddressRecord" inverseName="inBCCField" inverseEntity="AddressRecord"/> + <relationship name="ccAddresses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="AddressRecord" inverseName="inCCField" inverseEntity="AddressRecord"/> + <relationship name="decryptionKey" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SecretKeyRecord" inverseName="decryptedMails" inverseEntity="SecretKeyRecord"/> <relationship name="fromAddress" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="AddressRecord" inverseName="inFromField" inverseEntity="AddressRecord"/> - <relationship name="toAddresses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="AddressRecord"/> + <relationship name="includedAttachments" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="AttachmentRecord" inverseName="isPartOfMail" inverseEntity="AttachmentRecord"/> + <relationship name="inFolder" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FolderRecord" inverseName="mailsInFolder" inverseEntity="FolderRecord"/> + <relationship name="signatureKey" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PublicKeyRecord" inverseName="signedMails" inverseEntity="PublicKeyRecord"/> + <relationship name="toAddresses" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="AddressRecord" inverseName="inToField" inverseEntity="AddressRecord"/> <uniquenessConstraints> <uniquenessConstraint> <constraint value="messageID"/> @@ -38,17 +74,23 @@ <attribute name="lastSeenSignedMail" optional="YES" attributeType="Date" usesScalarValueType="NO"/> <attribute name="origin" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/> <attribute name="preferEncryption" attributeType="Integer 16" usesScalarValueType="YES"/> + <relationship name="sendWithAutocryptHeader" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MailRecord" inverseName="attachedAutocryptPublicKey" inverseEntity="MailRecord"/> + <relationship name="sendWithMail" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MailRecord" inverseName="attachedPublicKeys" inverseEntity="MailRecord"/> + <relationship name="signedMails" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MailRecord" inverseName="signatureKey" inverseEntity="MailRecord"/> </entity> <entity name="SecretKeyRecord" representedClassName="SecretKeyRecord" parentEntity="AbstractKeyRecord" syncable="YES" codeGenerationType="class"> <attribute name="exported" attributeType="Boolean" usesScalarValueType="YES"/> <attribute name="importedDate" optional="YES" attributeType="Date" usesScalarValueType="NO"/> + <relationship name="decryptedMails" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MailRecord" inverseName="decryptionKey" inverseEntity="MailRecord"/> + <relationship name="sendWithMail" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MailRecord" inverseName="attachedSecretKey" inverseEntity="MailRecord"/> </entity> - <fetchRequest name="FetchAddressesRequest" entity="AddressRecord" fetchLimit="200" fetchBatchSize="200"/> <elements> - <element name="AddressRecord" positionX="-36" positionY="27" width="128" height="73"/> - <element name="MailRecord" positionX="-54" positionY="-9" width="128" height="223"/> <element name="AbstractKeyRecord" positionX="-27" positionY="54" width="128" height="28"/> - <element name="SecretKeyRecord" positionX="-9" positionY="63" width="128" height="28"/> - <element name="PublicKeyRecord" positionX="9" positionY="63" width="128" height="28"/> + <element name="AddressRecord" positionX="-36" positionY="27" width="128" height="133"/> + <element name="FolderRecord" positionX="27" positionY="135" width="128" height="193"/> + <element name="MailRecord" positionX="-54" positionY="-9" width="128" height="328"/> + <element name="PublicKeyRecord" positionX="9" positionY="63" width="128" height="163"/> + <element name="SecretKeyRecord" positionX="-9" positionY="63" width="128" height="103"/> + <element name="AttachmentRecord" positionX="0" positionY="144" width="128" height="133"/> </elements> </model> \ No newline at end of file diff --git a/enzevalos_iphone/persistentData/FolderRecord.swift b/enzevalos_iphone/persistentData/FolderRecord.swift new file mode 100644 index 0000000000000000000000000000000000000000..d49244ff700b5b75a2a7309c62b5fded8dc0b3cf --- /dev/null +++ b/enzevalos_iphone/persistentData/FolderRecord.swift @@ -0,0 +1,33 @@ +// +// FolderRecord.swift +// enzevalos_iphone +// +// Created by Oliver Wiese on 05.10.20. +// Copyright © 2020 fu-berlin. All rights reserved. +// + +extension FolderRecord { + static let entityName = "FolderRecord" + + func update(properties: FolderProperties) { + if let delimiter = properties.delimiter { + self.delimiter = delimiter + } + if let icon = icon { + self.icon = icon + } + if let uidValidity = properties.uidValidity { + self.uidValidity = uidValidity + } + if let lastUpdate = properties.lastUpdate { + self.lastUpdate = lastUpdate + } + if let maxUID = properties.maxUID { + self.maxUID = maxUID + } + if let minUID = properties.minUID { + self.minUID = minUID + } + path = properties.path + } +} diff --git a/enzevalos_iphone/persistentData/MailRecord.swift b/enzevalos_iphone/persistentData/MailRecord.swift index 326b950f1aa510f2d964e8d2cfc254bb394a91f2..798e055c0d8ee2ba3bb04cf457c6c8c74385f145 100644 --- a/enzevalos_iphone/persistentData/MailRecord.swift +++ b/enzevalos_iphone/persistentData/MailRecord.swift @@ -9,7 +9,7 @@ /** Managed object subclass extension for the mail entity. */ -extension MailRecord: PersistentDataProtocol { +extension MailRecord { static let entityName = "MailRecord" func update(with mailProperties: MailProperties) { @@ -18,62 +18,22 @@ extension MailRecord: PersistentDataProtocol { date = mailProperties.date flag = mailProperties.flags subject = mailProperties.subject + xMailer = mailProperties.xMailer // Content body = mailProperties.body // Security - signatureState = mailProperties.signatureState - encryptionState = mailProperties.encryptionState + signatureStateInt = mailProperties.signatureState + encryptionStateInt = mailProperties.encryptionState } - func update(mail: IncomingMail) { - } -} - -/** - A struct encapsulating the properties of a E-Mail. - TODO: Folder, routing information, gmailMessageID, gmailThreadID, ThreadID, signatureKey, decryptionKey, attachments, autocrypt, xMailer - - What about attached keys (public, secret keys) - */ -struct MailProperties: DataPropertyProtocol { - - var entityName = "MailRecord" - - - // Header properties - let messageID: String - let subject: String - let date: Date - let flags: Int16 - let from: AddressProperties - let to: [AddressProperties] - let cc: [AddressProperties] - let bcc: [AddressProperties] - - // Content properties - var body: String = "" - - // Security properties - let signatureState: Int16 - let encryptionState: Int16 - - - - var dictionary: [String: Any] { - var dic = [String: Any]() - dic["messageID"] = messageID - dic["subject"] = subject - return dic + var signatureState: SignatureState { + return SignatureState.init(rawValue: signatureStateInt) ?? .NoSignature } - func update(m: Any) -> Bool { - if let m = m as? MailRecord { - m.update(with: self) - return true - } - return false + var encryptionState: EncryptionState { + return EncryptionState.init(rawValue: encryptionStateInt) ?? .NoEncryption } } diff --git a/enzevalos_iphone/persistentData/PersistentDataProtocol.swift b/enzevalos_iphone/persistentData/PersistentDataProtocol.swift index 954e57af691c48f622caa29718a1ebd0ad8eb183..a1592b2e68fc90a0b91ff713db53d4b64b031ce9 100644 --- a/enzevalos_iphone/persistentData/PersistentDataProtocol.swift +++ b/enzevalos_iphone/persistentData/PersistentDataProtocol.swift @@ -6,18 +6,6 @@ // Copyright © 2020 fu-berlin. All rights reserved. // -protocol PersistentDataProtocol { - associatedtype T - - func update(with properties: T) -} - -protocol DataPropertyProtocol { - var entityName: String { get } - var dictionary: [String: Any] { get } - - func update(m: Any) -> Bool -} diff --git a/enzevalos_iphone/persistentData/PersistentDataProvider.swift b/enzevalos_iphone/persistentData/PersistentDataProvider.swift index 7576196d81c5e578aea9005482c909e3ee8bb37d..3239b5d4b344ffc69d632d661ac71c8543c9f505 100644 --- a/enzevalos_iphone/persistentData/PersistentDataProvider.swift +++ b/enzevalos_iphone/persistentData/PersistentDataProvider.swift @@ -167,15 +167,23 @@ class PersitentDataProvider { // Create a managed object on the private queue context. let mail = NSEntityDescription.insertNewObject(forEntityName: MailRecord.entityName, into: taskContext) as! MailRecord mail.update(with: m) - - let from = NSEntityDescription.insertNewObject(forEntityName: AddressRecord.entityName, into: taskContext) as! AddressRecord - from.update(with: m.from) // add relationships - mail.fromAddress = from + // From + if let sender = m.from { + let from = NSEntityDescription.insertNewObject(forEntityName: AddressRecord.entityName, into: taskContext) as! AddressRecord + from.update(with: sender) + mail.fromAddress = from + } // To, CC, BCC importManyAddresses(addrs: m.to, taskContext: taskContext, addTo: mail.addToToAddresses) importManyAddresses(addrs: m.cc, taskContext: taskContext, addTo: mail.addToCcAddresses) importManyAddresses(addrs: m.bcc, taskContext: taskContext, addTo: mail.addToBccAddresses) + + // Add to folder + let folder = NSEntityDescription.insertNewObject(forEntityName: FolderRecord.entityName, into: taskContext) as! FolderRecord + folder.update(properties: m.folder) + mail.inFolder = folder + } private func importManyAddresses(addrs: [AddressProperties], taskContext: NSManagedObjectContext, addTo: (AddressRecord) -> () ) { @@ -194,12 +202,11 @@ class PersitentDataProvider { func deleteAll(completionHandler: @escaping (Error?) -> Void) { let taskContext = newTaskContext() taskContext.perform { - for name in [AddressRecord.entityName, MailRecord.entityName] { + for name in [AddressRecord.entityName, MailRecord.entityName, FolderRecord.entityName] { let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: name) let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) batchDeleteRequest.resultType = .resultTypeCount - // Execute the batch insert guard let batchDeleteResult = try? taskContext.execute(batchDeleteRequest) as? NSBatchDeleteResult, batchDeleteResult.result != nil else { completionHandler(PersistentDataError.batchDeleteError) @@ -231,10 +238,25 @@ class PersitentDataProvider { return controller }() + lazy var fetchedFolderResultsControler: NSFetchedResultsController<FolderRecord> = { + let fetchRequest = NSFetchRequest<FolderRecord>(entityName: FolderRecord.entityName) + fetchRequest.sortDescriptors = [NSSortDescriptor(key: "path", ascending: false)] + let controller = NSFetchedResultsController(fetchRequest: fetchRequest, + managedObjectContext: persistentContainer.viewContext, + sectionNameKeyPath: nil, cacheName: nil) + // Perform the fetch. + do { + try controller.performFetch() + } catch { + fatalError("Unresolved error \(error)") + } + return controller + } () + lazy var fetchedMailResultsController: NSFetchedResultsController<MailRecord> = { - // Create a fetch request for the Quake entity sorted by time. + // Create a fetch request for the email entity sorted by time. let fetchRequest = NSFetchRequest<MailRecord>(entityName: MailRecord.entityName) // Create a fetched results controller and set its fetch request, context, and delegate. fetchRequest.sortDescriptors = [NSSortDescriptor(key: "messageID", ascending: false)] diff --git a/enzevalos_iphone/persistentData/Properties.swift b/enzevalos_iphone/persistentData/Properties.swift new file mode 100644 index 0000000000000000000000000000000000000000..96e18a9b683c5714830bbbac5565acd0ee44ed9c --- /dev/null +++ b/enzevalos_iphone/persistentData/Properties.swift @@ -0,0 +1,99 @@ +// +// Properties.swift +// enzevalos_iphone +// +// Created by Oliver Wiese on 05.10.20. +// Copyright © 2020 fu-berlin. All rights reserved. +// +protocol DataPropertyProtocol { + var entityName: String { get } + + func update(m: Any) -> Bool +} + +/** + A struct encapsulating the properties of a E-Mail. + TODO: Folder, routing information, gmailMessageID, gmailThreadID, ThreadID, signatureKey, decryptionKey, attachments, autocrypt, xMailer + + What about attached keys (public, secret keys) + */ +struct MailProperties: DataPropertyProtocol { + + let entityName = "MailRecord" + + + // Header properties + let messageID: String + let subject: String + let date: Date + let flags: Int16 + let from: AddressProperties? + var to: [AddressProperties] = [] + var cc: [AddressProperties] = [] + var bcc: [AddressProperties] = [] + let xMailer: String? = nil + + let folder: FolderProperties + + // Content properties + var body: String = "" + + // Security properties + let signatureState: Int16 + let encryptionState: Int16 + + func update(m: Any) -> Bool { + if let m = m as? MailRecord { + m.update(with: self) + return true + } + return false + } +} + +struct AddressProperties: DataPropertyProtocol { + let entityName = "AddressRecord" + + let email: String + var name: String? = nil + + func update(m: Any) -> Bool { + if let m = m as? AddressRecord { + m.update(with: self) + return true + } + return false + } +} + +struct FolderProperties: DataPropertyProtocol{ + let entityName = "FolderRecord" + + var delimiter: String? + var icon: String? + var uidValidity: NSDecimalNumber? + + var lastUpdate: Date? + var maxUID: NSDecimalNumber? + var minUID: NSDecimalNumber? + + let path: String + + + func update(m: Any) -> Bool { + if let folder = m as? FolderRecord { + folder.update(properties: self) + return true + } + return false + } + + +} + + +extension MCOAddress { + func export() -> AddressProperties { + return AddressProperties(email: self.mailbox) + } +} diff --git a/enzevalos_iphoneTests/CoreAddressTest.swift b/enzevalos_iphoneTests/CoreAddressTest.swift index f0daa68a41287f26b558a4fee97788581daa793a..712a262f33c6a6992fef39e7b560694a1a6a8cd1 100644 --- a/enzevalos_iphoneTests/CoreAddressTest.swift +++ b/enzevalos_iphoneTests/CoreAddressTest.swift @@ -12,37 +12,130 @@ import XCTest class CoreAddressTest: XCTestCase { var provider = PersitentDataProvider() + private func deleteAll() { + provider = PersitentDataProvider() + let deleteExpectation = expectation(description: "Delete all data!") + provider.deleteAll(completionHandler: {error in + if let error = error { + XCTFail("Error while importing addresses! \(error)") + } + deleteExpectation.fulfill() + }) + wait(for: [deleteExpectation], timeout: TimeInterval(20)) + provider.reset() + let frc = provider.fetchAddressResultController + XCTAssertEqual(frc.fetchedObjects?.count ?? 0, 0) + provider = PersitentDataProvider() + } + override func setUpWithError() throws { - provider.deleteAll(completionHandler: {_ in }) + deleteAll() } override func tearDownWithError() throws { - provider.deleteAll(completionHandler: {_ in }) + deleteAll() } + func testSimpleCreateAddress() throws { + var addr1 = AddressProperties(email: "vic@example.com") + addr1.name = "vic" + let importExpectation = expectation(description: "Import new addresses!") + print("Import data!") + provider.importData(from: [addr1], completionHandler: {error in + if let error = error { + XCTFail("Error while importing addresses! \(error)") + } + importExpectation.fulfill() + }) + wait(for: [importExpectation], timeout: TimeInterval(20)) + provider.reset() + let frc = provider.fetchAddressResultController + XCTAssertEqual(frc.fetchedObjects?.count ?? 0, 1, "Missing addresses! \(frc.fetchedObjects?.count ?? 0)") + if let objects = frc.fetchedObjects { + for obj in objects { + XCTAssertEqual(obj.inFromField?.count, 0) + XCTAssertEqual(obj.email, "vic@example.com") + XCTAssertEqual(obj.displayname, "vic") + } + } else { + XCTFail("No addresses!") + } + + } + func testCreateAddress() throws { let n = 1 - let addr1 = AddressProperties(email: "vic@example.com") + var addr1 = AddressProperties(email: "vic@example.com") + addr1.name = "vic" let addr2 = AddressProperties(email: "vic@example.com") - let importExpectation = expectation(description: "Import new addresses!") - let addresses = [addr1, addr2] + var importExpectation = expectation(description: "Import new addresses!") - provider.importData(from: addresses, completionHandler: {error in + provider.importData(from: [addr1], completionHandler: {error in if let error = error { XCTFail("Error while importing addresses! \(error)") } importExpectation.fulfill() }) wait(for: [importExpectation], timeout: TimeInterval(20)) + + importExpectation = expectation(description: "Import new addresses!") + + provider.importData(from: [addr2], completionHandler: {error in + if let error = error { + XCTFail("Error while importing addresses! \(error)") + } + importExpectation.fulfill() + }) + wait(for: [importExpectation], timeout: TimeInterval(20)) provider.reset() let frc = provider.fetchAddressResultController - XCTAssertEqual(frc.fetchedObjects?.count ?? 0, n, "Missing message! \(frc.fetchedObjects?.count ?? 0)") + XCTAssertEqual(frc.fetchedObjects?.count ?? 0, n, "Missing addresses! \(frc.fetchedObjects?.count ?? 0)") if let objects = frc.fetchedObjects { for obj in objects { - print("#from mails: \(obj.inFromField?.count ?? -1)") + XCTAssertEqual(obj.inFromField?.count, 0) + XCTAssertEqual(obj.email, "vic@example.com") + XCTAssertEqual(obj.displayname, nil) } } + } + + func testCreateAddress2() throws { + let n = 1 + + let addr1 = AddressProperties(email: "vic@example.com") + var addr2 = AddressProperties(email: "vic@example.com") + addr2.name = "vic" + + var importExpectation = expectation(description: "Import new addresses!") + + provider.importData(from: [addr1], completionHandler: {error in + if let error = error { + XCTFail("Error while importing addresses! \(error)") + } + importExpectation.fulfill() + }) + wait(for: [importExpectation], timeout: TimeInterval(20)) + importExpectation = expectation(description: "Import new addresses!") + + provider.importData(from: [addr2], completionHandler: {error in + if let error = error { + XCTFail("Error while importing addresses! \(error)") + } + importExpectation.fulfill() + }) + wait(for: [importExpectation], timeout: TimeInterval(20)) + + provider.reset() + let frc = provider.fetchAddressResultController + XCTAssertEqual(frc.fetchedObjects?.count ?? 0, n, "Missing addresses! \(frc.fetchedObjects?.count ?? 0)") + if let objects = frc.fetchedObjects { + for obj in objects { + XCTAssertEqual(obj.inFromField?.count, 0) + XCTAssertEqual(obj.email, "vic@example.com") + XCTAssertEqual(obj.displayname, "vic") + } + } } } diff --git a/enzevalos_iphoneTests/CoreMailTest.swift b/enzevalos_iphoneTests/CoreMailTest.swift index e198cd51eaff3fa61b43691097aad212e26cd395..1d98c6884c880346a6e8126b2df2297cfb0fb5f0 100644 --- a/enzevalos_iphoneTests/CoreMailTest.swift +++ b/enzevalos_iphoneTests/CoreMailTest.swift @@ -13,12 +13,29 @@ class CoreMailTest: XCTestCase { var provider = PersitentDataProvider() + + private func deleteAll() { + provider = PersitentDataProvider() + let deleteExpectation = expectation(description: "Delete all data!") + provider.deleteAll(completionHandler: {error in + if let error = error { + XCTFail("Error while importing addresses! \(error)") + } + deleteExpectation.fulfill() + }) + wait(for: [deleteExpectation], timeout: TimeInterval(20)) + provider.reset() + let frc = provider.fetchedMailResultsController + XCTAssertEqual(frc.fetchedObjects?.count ?? 0, 0) + provider = PersitentDataProvider() + } + override func setUpWithError() throws { - provider.deleteAll(completionHandler: {_ in }) + deleteAll() } override func tearDownWithError() throws { - provider.deleteAll(completionHandler: {_ in }) + deleteAll() } /** @@ -27,10 +44,12 @@ class CoreMailTest: XCTestCase { func testImportMail(){ let addr = "vic@example.com" let addr1 = AddressProperties(email: addr) + let folder = FolderProperties(delimiter: nil, icon: nil, uidValidity: nil, lastUpdate: nil, maxUID: nil, minUID: nil, path: "Testfolder") + let importExpectation = expectation(description: "Import new msg!") - let m1 = MailProperties(messageID: "1", subject: "MSG1", date: Date(), flags: 0, from: addr1, body: "THIS IS MY SECOND MSG1", signatureState: 0, encryptionState: 0) - let m2 = MailProperties(messageID: "2", subject: "MSG2", date: Date(), flags: 0, from: addr1, body: "THIS IS MY SECOND MSG1", signatureState: 0, encryptionState: 0) + let m1 = MailProperties(messageID: "1", subject: "MSG1", date: Date(), flags: 0, from: addr1, folder: folder, signatureState: 0, encryptionState: 0) + let m2 = MailProperties(messageID: "2", subject: "MSG2", date: Date(), flags: 0, from: addr1, folder: folder, signatureState: 0, encryptionState: 0) provider.importMails(from: [m1,m2], completionHandler: {error in if let error = error { @@ -60,8 +79,10 @@ class CoreMailTest: XCTestCase { func testDuplicateMails() { let addr1 = AddressProperties(email: "vic@example.com") - let m1 = MailProperties(messageID: "1", subject: "MSG1", date: Date(), flags: 0, from: addr1, body: "THIS IS MY MSG1" , signatureState: 0, encryptionState: 0) - let m2 = MailProperties(messageID: "1", subject: "Second MSG1", date: Date(), flags: 0, from: addr1, body: "THIS IS MY SECOND MSG1" , signatureState: 0, encryptionState: 0) + let folder = FolderProperties(delimiter: nil, icon: nil, uidValidity: nil, lastUpdate: nil, maxUID: nil, minUID: nil, path: "Testfolder") + + let m1 = MailProperties(messageID: "1", subject: "MSG1", date: Date(), flags: 0, from: addr1, folder: folder, signatureState: 0, encryptionState: 0) + let m2 = MailProperties(messageID: "1", subject: "Second MSG1", date: Date(), flags: 0, from: addr1, folder: folder, signatureState: 0, encryptionState: 0) provider.reset() let frc = provider.fetchedMailResultsController @@ -81,9 +102,49 @@ class CoreMailTest: XCTestCase { XCTFail("Could not fetch! \(error)") } XCTAssertEqual(frc.fetchedObjects?.count ?? 0, 1, "Missing message! \(frc.fetchedObjects?.count ?? 0)") - if let obj = frc.fetchedObjects?.first { - XCTAssertEqual(obj.body, "THIS IS MY SECOND MSG1","We updated the message...") - XCTAssertEqual(obj.subject, "Second MSG1", "We updated the message...") + if let obj = frc.fetchedObjects?.first, let subject = obj.subject { + XCTAssert(subject == "Second MSG1" || subject == "MSG1", "Wired subject: \(subject)") + } else { + XCTFail("No messages!") + } + } + + func testDuplicateMails2() { + let addr1 = AddressProperties(email: "vic@example.com") + let folder = FolderProperties(delimiter: nil, icon: nil, uidValidity: nil, lastUpdate: nil, maxUID: nil, minUID: nil, path: "Testfolder") + + let m1 = MailProperties(messageID: "1", subject: "MSG1", date: Date(), flags: 0, from: addr1, folder: folder, signatureState: 0, encryptionState: 0) + let m2 = MailProperties(messageID: "1", subject: "Second MSG1", date: Date(), flags: 0, from: addr1, folder: folder, signatureState: 0, encryptionState: 0) + + provider.reset() + let frc = provider.fetchedMailResultsController + XCTAssertEqual(frc.fetchedObjects?.count ?? 0, 0, "Too much messages! \(frc.fetchedObjects?.count ?? 0)") + var importExpectation = expectation(description: "Import new data!") + provider.importMails(from: [m1], completionHandler: {error in + if let error = error { + XCTFail("Error while importing messages! \(error)") + } + importExpectation.fulfill() + }) + wait(for: [importExpectation], timeout: TimeInterval(20)) + + importExpectation = expectation(description: "Import new data!") + provider.importMails(from: [m2], completionHandler: {error in + if let error = error { + XCTFail("Error while importing messages! \(error)") + } + importExpectation.fulfill() + }) + wait(for: [importExpectation], timeout: TimeInterval(20)) + provider.reset() + do { + try frc.performFetch() + } catch { + XCTFail("Could not fetch! \(error)") + } + XCTAssertEqual(frc.fetchedObjects?.count ?? 0, 1, "Missing message! \(frc.fetchedObjects?.count ?? 0)") + if let obj = frc.fetchedObjects?.first, let subject = obj.subject { + XCTAssertEqual(subject, "Second MSG1", "We updated the message...") } else { XCTFail("No messages!") } @@ -91,23 +152,25 @@ class CoreMailTest: XCTestCase { } + /** + Tests to import multiple Mails in one Fetch + */ func testImportMails() { - let n = 100 + let n = 1000 let addr1 = AddressProperties(email: "vic@example.com") let addr2 = AddressProperties(email: "alex@example.com") let addrs = [addr1, addr2] + let folder = FolderProperties(delimiter: nil, icon: nil, uidValidity: nil, lastUpdate: nil, maxUID: nil, minUID: nil, path: "Testfolder") + var msgs = [MailProperties]() for i in 1...n { let a = addrs[i%addrs.count] - let m = MailProperties(messageID: "\(i)", subject: "MSG\(i)", date: Date(), flags: 0, from: a, body: "THIS IS MSG\(i)", signatureState: 0, encryptionState: 0) + let m = MailProperties(messageID: "\(i)", subject: "MSG\(i)", date: Date(), flags: 0, from: a, folder: folder, signatureState: 0, encryptionState: 0) msgs.append(m) } - let m = MailProperties(messageID: "1", subject: "Second MSG1", date: Date(), flags: 0, from: addr1, body: "THIS IS MY SECOND MSG1" , signatureState: 0, encryptionState: 0) - - msgs.append(m) provider.reset() let frc = provider.fetchedMailResultsController XCTAssertEqual(frc.fetchedObjects?.count ?? 0, 0, "Too much messages! \(frc.fetchedObjects?.count ?? 0)") @@ -126,5 +189,69 @@ class CoreMailTest: XCTestCase { XCTFail("Could not fetch! \(error)") } XCTAssertEqual(frc.fetchedObjects?.count ?? 0, n, "Missing message! \(frc.fetchedObjects?.count ?? 0)") + if let folders = provider.fetchedFolderResultsControler.fetchedObjects { + XCTAssert(folders.count == 1, "We have only one folder! folders: \(folders.count)") + if let f = folders.first { + XCTAssertEqual(f.path, "Testfolder") + XCTAssertEqual(f.mailsInFolder?.count, n) + } + } else{ + XCTFail("No folders!") + } + } + + + func testImportMoreMails() { + let n = 100 + let m = 10 + var k = 1 + + let addr1 = AddressProperties(email: "vic@example.com") + let addr2 = AddressProperties(email: "alex@example.com") + let addrs = [addr1, addr2] + let folder = FolderProperties(delimiter: nil, icon: nil, uidValidity: nil, lastUpdate: nil, maxUID: nil, minUID: nil, path: "Testfolder") + + for j in 1...m { + var msgs = [MailProperties]() + for i in 1...n { + let a = addrs[i%addrs.count] + let msg = MailProperties(messageID: "\(k)", subject: "MSG\(k)", date: Date(), flags: 0, from: a, folder: folder, signatureState: 0, encryptionState: 0) + k = k + 1 + msgs.append(msg) + } + + provider.reset() + let frc = provider.fetchedMailResultsController + XCTAssertEqual(frc.fetchedObjects?.count ?? 0, (j-1)*n, "Too much messages! \(frc.fetchedObjects?.count ?? 0)") + let importExpectation = expectation(description: "Import new data!") + provider.importMails(from: msgs, completionHandler: {error in + if let error = error { + XCTFail("Error while importing messages! \(error)") + } + importExpectation.fulfill() + }) + wait(for: [importExpectation], timeout: TimeInterval(20)) + provider.reset() + do { + try frc.performFetch() + } catch { + XCTFail("Could not fetch! \(error)") + } + XCTAssertEqual(frc.fetchedObjects?.count ?? 0, j*n, "Missing message! \(frc.fetchedObjects?.count ?? 0)") + } + + + if let folders = provider.fetchedFolderResultsControler.fetchedObjects { + XCTAssert(folders.count == 1, "We have only one folder! folders: \(folders.count)") + if let f = folders.first { + XCTAssertEqual(f.path, "Testfolder") + XCTAssertEqual(f.mailsInFolder?.count, m*n) + } + else{ + XCTFail("No folders!") + } + } else{ + XCTFail("No folders!") + } } } diff --git a/enzevalos_iphoneTests/GeneratedMocks.swift b/enzevalos_iphoneTests/GeneratedMocks.swift index 56ce0e296ffb22209e99def1367237630662624c..a0fa61aa2c6e68e278b2700bd6d8ef70cf0e6b6d 100644 --- a/enzevalos_iphoneTests/GeneratedMocks.swift +++ b/enzevalos_iphoneTests/GeneratedMocks.swift @@ -1,4 +1,4 @@ -// MARK: - Mocks generated from file: enzevalos_iphone/AuthenticationModel.swift at 2020-10-02 11:51:02 +0000 +// MARK: - Mocks generated from file: enzevalos_iphone/AuthenticationModel.swift at 2020-10-05 15:19:11 +0000 // // AuthenticationModel.swift @@ -654,7 +654,7 @@ import Foundation } -// MARK: - Mocks generated from file: enzevalos_iphone/AuthenticationViewModel.swift at 2020-10-02 11:51:02 +0000 +// MARK: - Mocks generated from file: enzevalos_iphone/AuthenticationViewModel.swift at 2020-10-05 15:19:11 +0000 // // AuthenticationViewModel.swift