diff --git a/enzevalos_iphone.xcodeproj/project.pbxproj b/enzevalos_iphone.xcodeproj/project.pbxproj index 262cbaeb67b38fc344adc40c93de0e5952a7319f..bdfaad849cc607f19adb8c8fc06bdcdcf9d439fd 100644 --- a/enzevalos_iphone.xcodeproj/project.pbxproj +++ b/enzevalos_iphone.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ 3FB75DC525FFA75C00919925 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB75DC425FFA75B00919925 /* ComposeView.swift */; }; 3FB75DC925FFA77800919925 /* RecipientRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB75DC825FFA77800919925 /* RecipientRowView.swift */; }; 3FB75DCD25FFD37400919925 /* RecipientListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FB75DCC25FFD37400919925 /* RecipientListView.swift */; }; + 3FEFF41E260A899100A7F9CC /* ComposeHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FEFF41D260A899100A7F9CC /* ComposeHeaderView.swift */; }; 4705E31A25AE36710065FF90 /* SMIMEMailTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4705E31925AE36710065FF90 /* SMIMEMailTest.swift */; }; 4706D65F225B7B6B00B3F1D3 /* ItunesHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4706D65E225B7B6B00B3F1D3 /* ItunesHandler.swift */; }; 4706D661225CD21D00B3F1D3 /* ExportKeyHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4706D660225CD21D00B3F1D3 /* ExportKeyHelper.swift */; }; @@ -85,7 +86,7 @@ 477120AE254C28F900B28C64 /* TabSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 477120AD254C28F900B28C64 /* TabSupport.swift */; }; 477120BC254C401E00B28C64 /* AddressRecord+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 477120BB254C401E00B28C64 /* AddressRecord+CoreDataProperties.swift */; }; 477120C2254C676000B28C64 /* ContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 477120C1254C676000B28C64 /* ContactView.swift */; }; - 477120CD254C76AE00B28C64 /* FolderOverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 477120CC254C76AE00B28C64 /* FolderOverView.swift */; }; + 477120CD254C76AE00B28C64 /* FolderListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 477120CC254C76AE00B28C64 /* FolderListView.swift */; }; 4774DD7322CFFD0E00BD8CF6 /* AliceMultiIDs (439EE43C) – Public.asc in Resources */ = {isa = PBXBuildFile; fileRef = 4774DD7222CFFD0E00BD8CF6 /* AliceMultiIDs (439EE43C) – Public.asc */; }; 4774DD7522D0015F00BD8CF6 /* multiIDs.eml in Resources */ = {isa = PBXBuildFile; fileRef = 4774DD7422D0015F00BD8CF6 /* multiIDs.eml */; }; 4774DD7B22D3F5D100BD8CF6 /* multiIDs2.eml in Resources */ = {isa = PBXBuildFile; fileRef = 4774DD7A22D3F5D100BD8CF6 /* multiIDs2.eml */; }; @@ -153,7 +154,7 @@ 47C112CA2531E9B000621A07 /* AttachmentRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C112C92531E9B000621A07 /* AttachmentRecord.swift */; }; 47C22281218AFD6300BD2C2B /* AutocryptTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C22280218AFD6300BD2C2B /* AutocryptTest.swift */; }; 47C22283218B02C700BD2C2B /* autocryptSimpleExample1.eml in Resources */ = {isa = PBXBuildFile; fileRef = 47C22282218B02C700BD2C2B /* autocryptSimpleExample1.eml */; }; - 47C3490225489F52008D290C /* MailRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C3490125489F52008D290C /* MailRow.swift */; }; + 47C3490225489F52008D290C /* MailRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C3490125489F52008D290C /* MailRowView.swift */; }; 47C8225324379EAE005BCE73 /* AttachmentsViewMain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C8224324379EAE005BCE73 /* AttachmentsViewMain.swift */; }; 47C8225724379EAE005BCE73 /* DialogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C8224924379EAE005BCE73 /* DialogView.swift */; }; 47C8225824379EAE005BCE73 /* FloatingActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C8224A24379EAE005BCE73 /* FloatingActionButton.swift */; }; @@ -194,7 +195,7 @@ 47F867E02052B47C00AA832F /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47F867DF2052B47C00AA832F /* Security.framework */; }; 47F867E22052B48E00AA832F /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 47F867E12052B48E00AA832F /* libz.tbd */; }; 47F867E42052B49800AA832F /* libbz2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 47F867E32052B49800AA832F /* libbz2.tbd */; }; - 47FA8EA8254C7E5B006883D0 /* FolderRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FA8EA7254C7E5B006883D0 /* FolderRow.swift */; }; + 47FA8EA8254C7E5B006883D0 /* FolderRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FA8EA7254C7E5B006883D0 /* FolderRowView.swift */; }; 47FA8EAC254D77DE006883D0 /* MailListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FA8EAB254D77DE006883D0 /* MailListView.swift */; }; 47FA8EC3254D9E01006883D0 /* RecipientFieldModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47FA8EC2254D9E01006883D0 /* RecipientFieldModel.swift */; }; 47FAE30E2524AA97005A1BCB /* DataModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 47FAE30C2524AA97005A1BCB /* DataModel.xcdatamodeld */; }; @@ -304,6 +305,7 @@ 3FB75DC425FFA75B00919925 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; }; 3FB75DC825FFA77800919925 /* RecipientRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientRowView.swift; sourceTree = "<group>"; }; 3FB75DCC25FFD37400919925 /* RecipientListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecipientListView.swift; sourceTree = "<group>"; }; + 3FEFF41D260A899100A7F9CC /* ComposeHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeHeaderView.swift; sourceTree = "<group>"; }; 434D0E988CDFB6D2D46C1EA8 /* Pods_enzevalos_iphoneUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_enzevalos_iphoneUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4705E31925AE36710065FF90 /* SMIMEMailTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMIMEMailTest.swift; sourceTree = "<group>"; }; 4706D65E225B7B6B00B3F1D3 /* ItunesHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItunesHandler.swift; sourceTree = "<group>"; }; @@ -472,7 +474,7 @@ 477120AD254C28F900B28C64 /* TabSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabSupport.swift; sourceTree = "<group>"; }; 477120BB254C401E00B28C64 /* AddressRecord+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AddressRecord+CoreDataProperties.swift"; sourceTree = "<group>"; }; 477120C1254C676000B28C64 /* ContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactView.swift; sourceTree = "<group>"; }; - 477120CC254C76AE00B28C64 /* FolderOverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderOverView.swift; sourceTree = "<group>"; }; + 477120CC254C76AE00B28C64 /* FolderListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderListView.swift; sourceTree = "<group>"; }; 4774DD7222CFFD0E00BD8CF6 /* AliceMultiIDs (439EE43C) – Public.asc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "AliceMultiIDs (439EE43C) – Public.asc"; sourceTree = "<group>"; }; 4774DD7422D0015F00BD8CF6 /* multiIDs.eml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = multiIDs.eml; sourceTree = "<group>"; }; 4774DD7A22D3F5D100BD8CF6 /* multiIDs2.eml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = multiIDs2.eml; sourceTree = "<group>"; }; @@ -528,7 +530,7 @@ 47C112C92531E9B000621A07 /* AttachmentRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentRecord.swift; sourceTree = "<group>"; }; 47C22280218AFD6300BD2C2B /* AutocryptTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutocryptTest.swift; sourceTree = "<group>"; }; 47C22282218B02C700BD2C2B /* autocryptSimpleExample1.eml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = autocryptSimpleExample1.eml; sourceTree = "<group>"; }; - 47C3490125489F52008D290C /* MailRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MailRow.swift; path = enzevalos_iphone/SwiftUI/SupportingViews/MailRow.swift; sourceTree = SOURCE_ROOT; }; + 47C3490125489F52008D290C /* MailRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MailRowView.swift; path = enzevalos_iphone/SwiftUI/SupportingViews/MailRowView.swift; sourceTree = SOURCE_ROOT; }; 47C8224324379EAE005BCE73 /* AttachmentsViewMain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentsViewMain.swift; sourceTree = "<group>"; }; 47C8224924379EAE005BCE73 /* DialogView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DialogView.swift; sourceTree = "<group>"; }; 47C8224A24379EAE005BCE73 /* FloatingActionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatingActionButton.swift; sourceTree = "<group>"; }; @@ -568,7 +570,7 @@ 47F867DF2052B47C00AA832F /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 47F867E12052B48E00AA832F /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; }; 47F867E32052B49800AA832F /* libbz2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libbz2.tbd; path = usr/lib/libbz2.tbd; sourceTree = SDKROOT; }; - 47FA8EA7254C7E5B006883D0 /* FolderRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderRow.swift; sourceTree = "<group>"; }; + 47FA8EA7254C7E5B006883D0 /* FolderRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderRowView.swift; sourceTree = "<group>"; }; 47FA8EAB254D77DE006883D0 /* MailListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailListView.swift; sourceTree = "<group>"; }; 47FA8EC2254D9E01006883D0 /* RecipientFieldModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientFieldModel.swift; sourceTree = "<group>"; }; 47FAE30D2524AA97005A1BCB /* DataModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = DataModel.xcdatamodel; sourceTree = "<group>"; }; @@ -963,8 +965,8 @@ 476406892416B54D00C7D426 /* KeyRecordRow.swift */, 4764068B2416B54D00C7D426 /* InboxCoordinator.swift */, 4750BDE02539C5FC00F6D5AB /* InboxView.swift */, - 47C3490125489F52008D290C /* MailRow.swift */, 47FA8EAB254D77DE006883D0 /* MailListView.swift */, + 47C3490125489F52008D290C /* MailRowView.swift */, ); path = Inbox; sourceTree = "<group>"; @@ -1042,8 +1044,8 @@ 477120CB254C766F00B28C64 /* FolderView */ = { isa = PBXGroup; children = ( - 477120CC254C76AE00B28C64 /* FolderOverView.swift */, - 47FA8EA7254C7E5B006883D0 /* FolderRow.swift */, + 477120CC254C76AE00B28C64 /* FolderListView.swift */, + 47FA8EA7254C7E5B006883D0 /* FolderRowView.swift */, ); path = FolderView; sourceTree = "<group>"; @@ -1230,6 +1232,7 @@ 3FB75DCC25FFD37400919925 /* RecipientListView.swift */, 47E04DDE255D3CF600189320 /* ComposeModel.swift */, 3FB75DC425FFA75B00919925 /* ComposeView.swift */, + 3FEFF41D260A899100A7F9CC /* ComposeHeaderView.swift */, ); path = Compose; sourceTree = "<group>"; @@ -1908,6 +1911,7 @@ 97BDE0432429188500B0BF03 /* BadgeProgressView.swift in Sources */, 47C822682438A85C005BCE73 /* SenderDetails.swift in Sources */, 47C8225824379EAE005BCE73 /* FloatingActionButton.swift in Sources */, + 3FEFF41E260A899100A7F9CC /* ComposeHeaderView.swift in Sources */, 47B71AAD2538354A00CA87C6 /* NewOnboardingView.swift in Sources */, AD97DFBE241F97A300C35B95 /* OnboardingIntroInfoSection.swift in Sources */, 47BCAF40259CFE110008FE4B /* AddButton.swift in Sources */, @@ -1945,8 +1949,7 @@ 3EC35F2420037651008BDF95 /* InvitationHelper.swift in Sources */, 47C112C22531D72E00621A07 /* PublicKeyRecord.swift in Sources */, 3FB75DCD25FFD37400919925 /* RecipientListView.swift in Sources */, - 472F39901E252470009260FB /* CNMailAddressesExtension.swift in Sources */, - 477120CD254C76AE00B28C64 /* FolderOverView.swift in Sources */, + 477120CD254C76AE00B28C64 /* FolderListView.swift in Sources */, 4733B202252B142C00AB5600 /* Properties.swift in Sources */, 47FAE3492524FB58005A1BCB /* AddressRecord.swift in Sources */, 4775D7AA243F0E260052F2CC /* SimulatorData.swift in Sources */, @@ -1973,7 +1976,7 @@ 47BCAF70259F9E390008FE4B /* PasswordAlert.swift in Sources */, 477120C2254C676000B28C64 /* ContactView.swift in Sources */, 47EABF09241A9C8700774A93 /* AuthenticationModel.swift in Sources */, - 47FA8EA8254C7E5B006883D0 /* FolderRow.swift in Sources */, + 47FA8EA8254C7E5B006883D0 /* FolderRowView.swift in Sources */, A1EB05881D956879008659C1 /* ContactHandler.swift in Sources */, 47EABF2D2423C65F00774A93 /* AuthenticationView.swift in Sources */, 47BCAF68259F48840008FE4B /* TempKeyRow.swift in Sources */, @@ -1992,7 +1995,7 @@ 47E04DDF255D3CF600189320 /* ComposeModel.swift in Sources */, 0EF73F4324237E6500932FA0 /* SMIMEHelpers.swift in Sources */, 47C8225E24379EAE005BCE73 /* ReadMainView.swift in Sources */, - 47C3490225489F52008D290C /* MailRow.swift in Sources */, + 47C3490225489F52008D290C /* MailRowView.swift in Sources */, 47FAE3122524BFDB005A1BCB /* PersistentDataError.swift in Sources */, 4750BDE12539C5FC00F6D5AB /* InboxView.swift in Sources */, 4750BDCE2539B54E00F6D5AB /* PermissionRequestView.swift in Sources */, diff --git a/enzevalos_iphone.xcodeproj/xcshareddata/xcschemes/enzevalos_iphone-AdHoc.xcscheme b/enzevalos_iphone.xcodeproj/xcshareddata/xcschemes/enzevalos_iphone-AdHoc.xcscheme index 048d12f1046c44ad3f7300bf30f080b5ec488c91..9ca67c64ee34427c88c359094b41a5874b9129f1 100644 --- a/enzevalos_iphone.xcodeproj/xcshareddata/xcschemes/enzevalos_iphone-AdHoc.xcscheme +++ b/enzevalos_iphone.xcodeproj/xcshareddata/xcschemes/enzevalos_iphone-AdHoc.xcscheme @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme - LastUpgradeVersion = "1230" + LastUpgradeVersion = "1240" version = "1.3"> <BuildAction parallelizeBuildables = "YES" diff --git a/enzevalos_iphone.xcodeproj/xcshareddata/xcschemes/enzevalos_iphone.xcscheme b/enzevalos_iphone.xcodeproj/xcshareddata/xcschemes/enzevalos_iphone.xcscheme index 85a049fc31b67f0e558e0b3f36d2d520a0cfcf87..5247072828caafc8171addb7791ac4cdf7d81600 100644 --- a/enzevalos_iphone.xcodeproj/xcshareddata/xcschemes/enzevalos_iphone.xcscheme +++ b/enzevalos_iphone.xcodeproj/xcshareddata/xcschemes/enzevalos_iphone.xcscheme @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="UTF-8"?> <Scheme - LastUpgradeVersion = "1230" + LastUpgradeVersion = "1240" version = "1.7"> <BuildAction parallelizeBuildables = "YES" diff --git a/enzevalos_iphone/DateExtension.swift b/enzevalos_iphone/DateExtension.swift index 8292566807c7d87662d6cd32b503754f3298075e..3086891821bd113c51a64ff511bbb6adc162cadf 100644 --- a/enzevalos_iphone/DateExtension.swift +++ b/enzevalos_iphone/DateExtension.swift @@ -7,8 +7,7 @@ // import Foundation -extension Date{ - +extension Date { func timeAgo() -> DateComponents { let calender = Calendar.current let start = calender.startOfDay(for: self) @@ -18,7 +17,11 @@ extension Date{ func isToday() -> Bool { let dateComponents = self.timeAgo() - if let day = dateComponents.day, let month = dateComponents.month, let year = dateComponents.year, day == 0 && month == 0, year == 0 { + if let day = dateComponents.day, + let month = dateComponents.month, + let year = dateComponents.year, + day == 0 && month == 0, + year == 0 { return true } return false @@ -29,28 +32,22 @@ extension Date{ if let years = components.year, years > 0 { if years == 1 { return NSLocalizedString("Time.Year.One", comment: "one year") - } - else { + } else { return String(format: NSLocalizedString("Time.Year.Multiple", comment: "multiple years"), years) } - } - else if let months = components.month, months > 0 { + } else if let months = components.month, months > 0 { if months == 1 { return NSLocalizedString("Time.Month.One", comment: "one month") - } - else { + } else { let s = NSLocalizedString("Time.Month.Multiple", comment: "multiple month") return String(format: s, months) } - } - else if let days = components.day { + } else if let days = components.day { if days == 0 { return NSLocalizedString("Time.Day.Today", comment: "today") - } - else if days == 1 { + } else if days == 1 { return NSLocalizedString("Time.Day.One", comment: "one day") - } - else { + } else { let s = NSLocalizedString("Time.Day.Multiple", comment: "multiple days") return String(format: s, days) } @@ -60,28 +57,26 @@ extension Date{ // TODO: Refactor -> Other name var dateToString: String { get { - var returnString = "" let dateFormatter = DateFormatter() dateFormatter.locale = Locale.current let mailTime = self let interval = Date().timeIntervalSince(mailTime as Date) switch interval { case -55..<55: - returnString = NSLocalizedString("Now", comment: "New email") + return NSLocalizedString("Now", comment: "New email") case 55..<120: - returnString = NSLocalizedString("OneMinuteAgo", comment: "Email came one minute ago") + return NSLocalizedString("OneMinuteAgo", comment: "Email came one minute ago") case 120..<24 * 60 * 60: dateFormatter.timeStyle = .short - returnString = dateFormatter.string(from: mailTime as Date) + return dateFormatter.string(from: mailTime as Date) case 86400..<172800: //24 * 60 * 60..<48 * 60 * 60: - returnString = NSLocalizedString("Yesterday", comment: "Email came yesterday") + return NSLocalizedString("Yesterday", comment: "Email came yesterday") case 172800..<259200: //48 * 60 * 60..<72 * 60 * 60: - returnString = NSLocalizedString("TwoDaysAgo", comment: "Email came two days ago") + return NSLocalizedString("TwoDaysAgo", comment: "Email came two days ago") default: dateFormatter.dateStyle = .short - returnString = dateFormatter.string(from: mailTime as Date) + return dateFormatter.string(from: mailTime as Date) } - return returnString } } } diff --git a/enzevalos_iphone/MailSession.swift b/enzevalos_iphone/MailSession.swift index c9ccdd74f0f193066d7bda33835b8b5208255a74..d4d6cf2c220c4df9f61b0b58177b71e7d8da64f8 100644 --- a/enzevalos_iphone/MailSession.swift +++ b/enzevalos_iphone/MailSession.swift @@ -32,7 +32,11 @@ class MailServer: Comparable { } static func == (lhs: MailServer, rhs: MailServer) -> Bool { - return lhs.sessionType == rhs.sessionType && lhs.hostname == rhs.hostname && lhs.port == rhs.port && lhs.connectionType == rhs.connectionType && lhs.authType == rhs.authType + return lhs.sessionType == rhs.sessionType + && lhs.hostname == rhs.hostname + && lhs.port == rhs.port + && lhs.connectionType == rhs.connectionType + && lhs.authType == rhs.authType } private static let maxWaitingSeconds = 15 @@ -60,16 +64,18 @@ class MailServer: Comparable { var possibleAuthTypes: [MCOAuthType] = [] var toTestConnType: Set<MCOConnectionType> = [] - var toTestServers: [(hostname: String, port: UInt32, authTyp: MCOAuthType, connType: MCOConnectionType)] = [] + var toTestServers: [(hostname: String, + port: UInt32, + authTyp: MCOAuthType, + connType: MCOConnectionType)] = [] var createdTestServers = false - var receivedAuthTypes = false var loggerCalled = false var works = false var sendCallback = false var isTLS: Bool { - get{ + get { return connectionType == MCOConnectionType.TLS } } @@ -90,9 +96,16 @@ class MailServer: Comparable { return msg } } - - init(sessionType: SessionType, username: String, password: String, hostname: String, port: UInt32, connectionType: MCOConnectionType?, authType: MCOAuthType? = nil, callback: @escaping (_ error: MailServerConnectionError?, _ server: MailServer) -> ()) { + + init(sessionType: SessionType, + username: String, + password: String, + hostname: String, + port: UInt32, + connectionType: MCOConnectionType?, + authType: MCOAuthType? = nil, + callback: @escaping (_ error: MailServerConnectionError?, _ server: MailServer) -> ()) { self.sessionType = sessionType self.hostname = hostname self.port = port @@ -101,78 +114,115 @@ class MailServer: Comparable { if let connectionType = connectionType { self.connectionType = connectionType self.toTestConnType = [] - } - else if port == 143 || port == 587 || port == 25 { + } else if port == 143 || port == 587 || port == 25 { self.connectionType = .startTLS - - } - else { - self.connectionType = .TLS + } else { + self.connectionType = .TLS } self.username = username self.password = password - self.callback = callback } - convenience init(sessionType: SessionType, username: String, password: String, serverConfig: MCONetService, callback: @escaping (_ error: MailServerConnectionError?, _ server: MailServer) -> ()) throws { - if let serverInfo = serverConfig.info(), let hostname = serverInfo["hostname"] as? String, let port = serverInfo["port"] as? UInt32 { + convenience init(sessionType: SessionType, + username: String, + password: String, + serverConfig: MCONetService, + callback: @escaping (_ error: MailServerConnectionError?, + _ server: MailServer) -> ()) throws { + if let serverInfo = serverConfig.info(), + let hostname = serverInfo["hostname"] as? String, + let port = serverInfo["port"] as? UInt32 { var conn: MCOConnectionType? = nil + if let trans = serverInfo["ssl"] as? Bool, trans { conn = MCOConnectionType.TLS } + if let trans = serverInfo["starttls"] as? Bool, trans { conn = MCOConnectionType.startTLS } - self.init(sessionType: sessionType, username: username, password: password, hostname: hostname, port: port, connectionType: conn, callback: callback) - } - else { - throw MailServerConnectionError.NoData + + self.init(sessionType: sessionType, + username: username, + password: password, + hostname: hostname, + port: port, + connectionType: conn, + callback: callback) + } else { + throw MailServerConnectionError.NoData } } - convenience init(userValues sessionType: SessionType, callback: @escaping (_ error: MailServerConnectionError?, _ server: MailServer) -> ()) throws{ + convenience init(userValues sessionType: SessionType, + callback: @escaping (_ error: MailServerConnectionError?, + _ server: MailServer) -> ()) throws { if let username = UserManager.loadUserValue(Attribute.accountname) as? String, - let password = UserManager.loadUserValue(Attribute.userPW) as? String { - if sessionType == SessionType.IMAP, let hostname = UserManager.loadUserValue(Attribute.imapHostname) as? String, let port = UserManager.loadUserValue(Attribute.imapPort) as? UInt32{ - var connType: MCOConnectionType? = nil - if let rawtype = UserManager.loadUserValue(Attribute.imapConnectionType) as? Int { - connType = MCOConnectionType.init(rawValue: rawtype) - } - var authType: MCOAuthType? = nil - if let rawtype = UserManager.loadUserValue(Attribute.smtpAuthType) as? Int { - authType = MCOAuthType.init(rawValue: rawtype) - } - self.init(sessionType: sessionType, username: username, password: password, hostname: hostname , port: port, connectionType: connType, authType: authType, callback: callback) - if let authType = UserManager.loadUserValue(Attribute.imapAuthType) as? Int { - self.authType = MCOAuthType.init(rawValue: authType) - } - + let password = UserManager.loadUserValue(Attribute.userPW) as? String { + if sessionType == SessionType.IMAP, + let hostname = UserManager.loadUserValue(Attribute.imapHostname) as? String, + let port = UserManager.loadUserValue(Attribute.imapPort) as? UInt32 { + var connType: MCOConnectionType? = nil + if let rawtype = UserManager.loadUserValue(Attribute.imapConnectionType) as? Int { + connType = MCOConnectionType.init(rawValue: rawtype) } - else if let hostname = UserManager.loadUserValue(Attribute.smtpHostname) as? String, let port = UserManager.loadUserValue(Attribute.smtpPort) as? UInt32 { - var connType: MCOConnectionType? = nil - if let rawtype = UserManager.loadUserValue(Attribute.smtpConnectionType) as? Int { - connType = MCOConnectionType.init(rawValue: rawtype) - } - var authType: MCOAuthType? = nil - if let rawtype = UserManager.loadUserValue(Attribute.smtpAuthType) as? Int { - authType = MCOAuthType.init(rawValue: rawtype) - } - self.init(sessionType: sessionType, username: username, password: password, hostname: hostname, port: port, connectionType: connType, authType: authType, callback: callback) - if let authType = UserManager.loadUserValue(Attribute.smtpAuthType) as? Int { - self.authType = MCOAuthType.init(rawValue: authType) - } + + var authType: MCOAuthType? = nil + if let rawtype = UserManager.loadUserValue(Attribute.smtpAuthType) as? Int { + authType = MCOAuthType.init(rawValue: rawtype) } - else { - throw MailServerConnectionError.NoData + + self.init(sessionType: sessionType, + username: username, + password: password, + hostname: hostname, + port: port, + connectionType: connType, + authType: authType, + callback: callback) + if let authType = UserManager.loadUserValue(Attribute.imapAuthType) as? Int { + self.authType = MCOAuthType.init(rawValue: authType) } - } - else { + } else if let hostname = UserManager.loadUserValue(Attribute.smtpHostname) as? String, + let port = UserManager.loadUserValue(Attribute.smtpPort) as? UInt32 { + var connType: MCOConnectionType? = nil + + if let rawtype = UserManager.loadUserValue(Attribute.smtpConnectionType) as? Int { + connType = MCOConnectionType.init(rawValue: rawtype) + } + + var authType: MCOAuthType? = nil + if let rawtype = UserManager.loadUserValue(Attribute.smtpAuthType) as? Int { + authType = MCOAuthType.init(rawValue: rawtype) + } + + self.init(sessionType: sessionType, + username: username, + password: password, + hostname: hostname, + port: port, + connectionType: connType, + authType: authType, + callback: callback) + + if let authType = UserManager.loadUserValue(Attribute.smtpAuthType) as? Int { + self.authType = MCOAuthType.init(rawValue: authType) + } + } else { + throw MailServerConnectionError.NoData + } + } else { throw MailServerConnectionError.NoData } } - convenience init(defaultValues sessionType: SessionType, mailAddr: String, username: String, password: String, callback: @escaping (_ error: MailServerConnectionError?, _ server: MailServer) -> ()) { + convenience init(defaultValues sessionType: SessionType, + mailAddr: String, + username: String, + password: String, + callback: @escaping (_ error: MailServerConnectionError?, + _ server: MailServer) -> ()) { var auth = MCOAuthType.init(rawValue: 0) var conn = MCOConnectionType.TLS var port: UInt32 = 111 @@ -186,43 +236,51 @@ class MailServer: Comparable { if let authTypeValue = Attribute.imapAuthType.defaultValue as? Int { auth = MCOAuthType.init(rawValue: authTypeValue) } + if let connectionTypeValue = Attribute.imapConnectionType.defaultValue as? Int { conn = MCOConnectionType.init(rawValue: connectionTypeValue) } + if let name = Attribute.imapHostname.defaultValue as? String, domain == nil { hostname = name + } else if !MailSession.IMAPPREFIX.isEmpty{ + hostname = MailSession.IMAPPREFIX[0] + "." + hostname } - else if !MailSession.IMAPPREFIX.isEmpty{ - hostname = MailSession.IMAPPREFIX[0]+"."+hostname - } + if let p = Attribute.imapPort.defaultValue as? UInt32 { port = p - } - else if !MailSession.IMAPPORT.isEmpty { + } else if !MailSession.IMAPPORT.isEmpty { port = MailSession.IMAPPORT[0] } - } - else { + } else { if let authTypeValue = Attribute.smtpAuthType.defaultValue as? Int { auth = MCOAuthType.init(rawValue: authTypeValue) } + if let connectionTypeValue = Attribute.smtpConnectionType.defaultValue as? Int { conn = MCOConnectionType.init(rawValue: connectionTypeValue) } + if let name = Attribute.smtpHostname.defaultValue as? String, domain == nil { hostname = name - } - else if !MailSession.SMTPPREFIX.isEmpty{ + } else if !MailSession.SMTPPREFIX.isEmpty{ hostname = MailSession.SMTPPREFIX[0]+"."+hostname } + if let p = Attribute.smtpPort.defaultValue as? UInt32 { port = p - } - else if !MailSession.SMTPPORT.isEmpty { + } else if !MailSession.SMTPPORT.isEmpty { port = MailSession.SMTPPORT[0] } } - self.init(sessionType: sessionType, username: username, password: password, hostname: hostname, port: port, connectionType: conn, authType: auth, callback: callback) + self.init(sessionType: sessionType, + username: username, + password: password, + hostname: hostname, + port: port, + connectionType: conn, + authType: auth, + callback: callback) } func storeToUserDefaults(mailAddr: String) -> Bool { @@ -230,91 +288,124 @@ class MailServer: Comparable { let connectionTypeValue = connectionType.rawValue let authTypeValue = authType.rawValue if sessionType == SessionType.IMAP { - UserManager.storeUserValue(hostname as AnyObject, attribute: Attribute.imapHostname) - UserManager.storeUserValue(port as AnyObject, attribute: Attribute.imapPort) - UserManager.storeUserValue(connectionTypeValue as AnyObject, attribute: Attribute.imapConnectionType) - UserManager.storeUserValue(authTypeValue as AnyObject, attribute: Attribute.imapAuthType) - } - else { - UserManager.storeUserValue(hostname as AnyObject, attribute: Attribute.smtpHostname) - UserManager.storeUserValue(port as AnyObject, attribute: Attribute.smtpPort) - UserManager.storeUserValue(connectionTypeValue as AnyObject, attribute: Attribute.smtpConnectionType) - UserManager.storeUserValue(authTypeValue as AnyObject, attribute: Attribute.smtpAuthType) - } - UserManager.storeUserValue(password as AnyObject, attribute: Attribute.userPW) - UserManager.storeUserValue(username as AnyObject, attribute: Attribute.accountname) - UserManager.storeUserValue(mailAddr.lowercased() as AnyObject, attribute: Attribute.userAddr) - UserManager.storeUserValue(mailAddr.lowercased() as AnyObject, attribute: Attribute.userDisplayName) + UserManager.storeUserValue(hostname as AnyObject, + attribute: Attribute.imapHostname) + UserManager.storeUserValue(port as AnyObject, + attribute: Attribute.imapPort) + UserManager.storeUserValue(connectionTypeValue as AnyObject, + attribute: Attribute.imapConnectionType) + UserManager.storeUserValue(authTypeValue as AnyObject, + attribute: Attribute.imapAuthType) + } else { + UserManager.storeUserValue(hostname as AnyObject, + attribute: Attribute.smtpHostname) + UserManager.storeUserValue(port as AnyObject, + attribute: Attribute.smtpPort) + UserManager.storeUserValue(connectionTypeValue as AnyObject, + attribute: Attribute.smtpConnectionType) + UserManager.storeUserValue(authTypeValue as AnyObject, + attribute: Attribute.smtpAuthType) + } + UserManager.storeUserValue(password as AnyObject, + attribute: Attribute.userPW) + UserManager.storeUserValue(username as AnyObject, + attribute: Attribute.accountname) + UserManager.storeUserValue(mailAddr.lowercased() as AnyObject, + attribute: Attribute.userAddr) + UserManager.storeUserValue(mailAddr.lowercased() as AnyObject, + attribute: Attribute.userDisplayName) return true } - return false + return false } - func createSMTPSession(logging: Bool = true, credentials: Bool = false, withAuthType: Bool = true) -> MCOSMTPSession? { + func createSMTPSession(logging: Bool = true, + credentials: Bool = false, + withAuthType: Bool = true) -> MCOSMTPSession? { loggerCalled = false guard sessionType == .SMTP else { return nil } let session = MCOSMTPSession() session.timeout = TimeInterval(MailServer.maxWaitingSeconds) - session.hostname = hostname.remove(seperatedBy: .whitespacesAndNewlines) session.port = port session.connectionType = connectionType + if logging { session.connectionLogger = parseLog } + if withAuthType, let auth = authType { session.authType = auth if session.authType == MCOAuthType.xoAuth2 { - session.oAuth2Token = EmailHelper.singleton().authorization?.authState.lastTokenResponse?.accessToken + session.oAuth2Token = EmailHelper + .singleton() + .authorization? + .authState + .lastTokenResponse? + .accessToken } } + if credentials { session.username = username.lowercased() session.password = password } + return session } - func createIMAPSession(logging: Bool = true, credentials: Bool = false, withAuthType: Bool = true) -> MCOIMAPSession? { + func createIMAPSession(logging: Bool = true, + credentials: Bool = false, + withAuthType: Bool = true) -> MCOIMAPSession? { loggerCalled = false + guard sessionType == .IMAP else { return nil } + let session = MCOIMAPSession() session.timeout = TimeInterval(MailServer.maxWaitingSeconds) - session.hostname = hostname.remove(seperatedBy: .whitespacesAndNewlines) session.port = port session.connectionType = connectionType + if logging { session.connectionLogger = parseLog } + if withAuthType, let auth = authType { session.authType = auth if session.authType == MCOAuthType.xoAuth2 { - session.oAuth2Token = EmailHelper.singleton().authorization?.authState.lastTokenResponse?.accessToken + session.oAuth2Token = EmailHelper + .singleton() + .authorization? + .authState + .lastTokenResponse? + .accessToken } } + if credentials { session.username = username session.password = password } + return session } func findHost() -> Bool{ - guard LetterboxModel.currentReachabilityStatus != .notReachable else { + guard LetterboxModel.currentReachabilityStatus != .notReachable else { sendCallback = true self.callback(MailServerConnectionError.NoInternetconnection, self) return false } + toTestConnType.remove(connectionType) if sessionType == SessionType.IMAP { return findIMAPHost() - } - else { + } else { if self.possibleAuthTypes.isEmpty { self.possibleAuthTypes = MailSession.AUTHTYPE } @@ -330,8 +421,7 @@ class MailServer: Comparable { if let newType = self.toTestConnType.popFirst() { self.connectionType = newType _ = self.findHost() - } - else if self.toTestConnType.isEmpty && !self.sendCallback { + } else if self.toTestConnType.isEmpty && !self.sendCallback { self.sendCallback = true self.callback(MailServerConnectionError.ImapSetupError, self) } @@ -351,12 +441,16 @@ class MailServer: Comparable { } private func findSMTPHost() -> Bool { - guard let session = createSMTPSession(logging: true, credentials: true, withAuthType: true) else { + guard let session = createSMTPSession(logging: true, + credentials: true, + withAuthType: true) else { return false } + guard let op = session.loginOperation() else { return false } + op.start({[unowned self] (error: Error?) -> () in guard error == nil else { self.failedAttempts = self.failedAttempts + 1 @@ -364,8 +458,7 @@ class MailServer: Comparable { let serverError = MailServerConnectionError.findErrorCode(error: error) if self.sendCallback(session: session, error: serverError) { self.callback(MailServerConnectionError.SmtpSetupError, self) - } - else { + } else { if !self.findSMTPHost() { self.callback(MailServerConnectionError.SmtpSetupError, self) } @@ -391,7 +484,10 @@ class MailServer: Comparable { return true } switch error { - case .AuthenticationError, .AuthenticationRequiredError, .CertificateError, .GmailIMAPNotEnabledError: + case .AuthenticationError, + .AuthenticationRequiredError, + .CertificateError, + .GmailIMAPNotEnabledError: return true default: if toTestServers.count == 0 && createdTestServers { @@ -406,7 +502,6 @@ class MailServer: Comparable { self.authType = next.authTyp self.connectionType = next.connType return false - } } @@ -417,7 +512,7 @@ class MailServer: Comparable { } createdTestServers = true } - + private func parseLog(logger: Any, type: MCOConnectionLogType, data: Data?) { self.loggerCalled = true @@ -441,7 +536,9 @@ class MailServer: Comparable { if let auth = possibleAuthTypes.last { possibleAuthTypes.removeLast() self.authType = auth - if let session = createIMAPSession(logging: true, credentials: false, withAuthType: true) { + if let session = createIMAPSession(logging: true, + credentials: false, + withAuthType: true) { if let x = session.connectOperation(){ x.start({[unowned self](error: Error?) -> () in guard error == nil else { @@ -470,18 +567,17 @@ class MailServer: Comparable { self.authType = self.possibleAuthTypes.removeLast() self.testUsernameAndPW() return - } - else if conError != MailServerConnectionError.AuthenticationError && (!self.receivedAuthTypes || !self.possibleAuthTypes.isEmpty) && !self.possibleAuthTypes.isEmpty{ + } else if conError != MailServerConnectionError.AuthenticationError + && (!self.receivedAuthTypes || !self.possibleAuthTypes.isEmpty) + && !self.possibleAuthTypes.isEmpty { self.authType = self.possibleAuthTypes.removeLast() self.testUsernameAndPW() return - } - else if !self.sendCallback { + } else if !self.sendCallback { self.sendCallback = true self.callback(conError, self) } - } - else { + } else { self.works = true if !self.sendCallback { self.sendCallback = true @@ -499,13 +595,24 @@ class MailSession { static let SMTPPREFIX = ["smtp", "mail", "outgoing", "", "mx"] static let IMAPPREFIX = ["imap", "mail", "", "mx"] static let IMAPPORT: [UInt32] = [993, 143] - static let AUTHTYPE = [MCOAuthType.saslPlain, MCOAuthType.saslLogin, MCOAuthType.SASLNTLM, MCOAuthType.saslKerberosV4, MCOAuthType.SASLCRAMMD5, MCOAuthType.SASLDIGESTMD5, MCOAuthType.SASLGSSAPI, MCOAuthType.SASLSRP, MCOAuthType.init(rawValue: 0)] - static let CONNTECTIONTYPE = [MCOConnectionType.TLS, MCOConnectionType.startTLS] // We do not test for plain connections! + static let AUTHTYPE = [MCOAuthType.saslPlain, + MCOAuthType.saslLogin, + MCOAuthType.SASLNTLM, + MCOAuthType.saslKerberosV4, + MCOAuthType.SASLCRAMMD5, + MCOAuthType.SASLDIGESTMD5, + MCOAuthType.SASLGSSAPI, + MCOAuthType.SASLSRP, + MCOAuthType.init(rawValue: 0)] + // We do not test for plain connections! + static let CONNTECTIONTYPE = [MCOConnectionType.TLS, MCOConnectionType.startTLS] var defaultIMAPSession: MCOIMAPSession? { get { if let server = try? MailServer(userValues: sessionType, callback: callback) { - if let session = server.createIMAPSession(logging: false, credentials: true, withAuthType: true) { + if let session = server.createIMAPSession(logging: false, + credentials: true, + withAuthType: true) { return session } } @@ -515,8 +622,11 @@ class MailSession { var defaultSMTPSession: MCOSMTPSession? { get { - if let server = try? MailServer(userValues: sessionType, callback: callback) , let session = server.createSMTPSession(logging: false, credentials: true, withAuthType: true){ - + if let server = try? MailServer(userValues: sessionType, + callback: callback), + let session = server.createSMTPSession(logging: false, + credentials: true, + withAuthType: true) { return session } return nil @@ -528,7 +638,7 @@ class MailSession { var username: String var password: String var errors: Set<MailServerConnectionError> = Set() - + private var possibleServers: [MailServer] = [] private var counter = -1 private var success = false @@ -555,40 +665,45 @@ class MailSession { return server } } - return MailServer(defaultValues: sessionType, mailAddr: mailAddr, username: username, password: password, callback: callback) + return MailServer(defaultValues: sessionType, + mailAddr: mailAddr, + username: username, + password: password, + callback: callback) } } - - - init(configSession sessionType: SessionType, mailAddress: String, password: String, username: String?) { + init(configSession sessionType: SessionType, + mailAddress: String, + password: String, + username: String?) { self.sessionType = sessionType self.mailAddr = mailAddress self.password = password if let name = username, !name.isEmpty { self.username = name - } - else { + } else { self.username = mailAddr } } init(loadUserDefaults sessionType: SessionType) throws{ self.sessionType = sessionType + if let username = UserManager.loadUserValue(Attribute.accountname) as? String { self.username = username - } - else if let username = UserManager.loadUserValue(Attribute.userAddr) as? String { + } else if let username = UserManager.loadUserValue(Attribute.userAddr) as? String { self.username = username - } - else { + } else { throw MailServerConnectionError.AuthenticationError } + if let pw = UserManager.loadUserValue(Attribute.userPW) as? String { self.password = pw - }else { + } else { throw MailServerConnectionError.AuthenticationError } + if let addr = UserManager.loadUserValue(Attribute.userAddr) as? String { mailAddr = addr } else { @@ -600,9 +715,8 @@ class MailSession { listeners.append(listener) } - /* - Stores a working server configuration to user defaults. - */ + + /// Stores a working server configuration to user defaults. func storeToUserDefaults() -> Bool { workingServers.sort() workingServers.reverse() @@ -627,7 +741,14 @@ class MailSession { if let rawValue = authType { auth = MCOAuthType.init(rawValue: rawValue) } - let server = MailServer(sessionType: sessionType, username: username, password: password, hostname: hostname, port: port, connectionType: MCOConnectionType.init(rawValue: connType), authType: auth, callback: callback) + let server = MailServer(sessionType: sessionType, + username: username, + password: password, + hostname: hostname, + port: port, + connectionType: MCOConnectionType.init(rawValue: connType), + authType: auth, + callback: callback) possibleServers.append(server) } @@ -635,18 +756,20 @@ class MailSession { return searchForServerConfig(hostFromAdr: false) } - /* - We test all common server configurations (common prefixe x common ports x {TLS, startTLS} ). This may take some time. - */ + // We test all common server configurations (common prefixe x common ports x {TLS, startTLS} ). + // This may take some time. func startLongSearchOfServerConfig(hostFromAdr: Bool) -> Bool { // Try to find a possible config let (_, server) = MailSession.splitAddr(userAddr: self.mailAddr) if let domain = server { if self.sessionType == SessionType.IMAP { - createServers(domain: domain, prefixes: MailSession.IMAPPREFIX, ports: [UInt32(111)]) - } - else { - createServers(domain: domain, prefixes: MailSession.SMTPPREFIX, ports: MailSession.SMTPPORT) + createServers(domain: domain, + prefixes: MailSession.IMAPPREFIX, + ports: [UInt32(111)]) + } else { + createServers(domain: domain, + prefixes: MailSession.SMTPPREFIX, + ports: MailSession.SMTPPORT) } } return searchForServerConfig(hostFromAdr: hostFromAdr) @@ -678,14 +801,23 @@ class MailSession { possibleServers = [] for prefix in prefixes { for port in ports { - // We check both connection types in parallel - // because it takes a long time if we start with the wrong conntection type (TLS or startTLS) + // We check both connection types in parallel because it takes a long + // time if we start with the wrong conntection type (TLS or startTLS) for conType in MailSession.CONNTECTIONTYPE { - var hostname = prefix+"."+domain + var hostname = prefix + "." + domain + if prefix.isEmpty { hostname = domain } - let server = MailServer(sessionType: sessionType, username: username, password: password, hostname: hostname, port: port, connectionType: conType, authType: nil, callback: callback) + + let server = MailServer(sessionType: sessionType, + username: username, + password: password, + hostname: hostname, + port: port, + connectionType: conType, + authType: nil, + callback: callback) possibleServers.append(server) } } @@ -702,13 +834,12 @@ class MailSession { } private func callback(error: MailServerConnectionError?, server: MailServer) { - DispatchQueue.main.async(execute:{ + DispatchQueue.main.async { if error == nil { self.success = true self.workingServers.append(server) self.notifyListener() - } - else { + } else { if let error = error { self.errors.insert(error) } @@ -718,7 +849,7 @@ class MailSession { self.notifyListener() } } - }) + } } private func readJson() -> [MCONetService] { @@ -729,8 +860,7 @@ class MailSession { if let provider = manager.provider(forEmail: mailAddr){ if sessionType == SessionType.IMAP { servers = provider.imapServices() - } - else { + } else { servers = provider.smtpServices() } } @@ -785,7 +915,7 @@ extension MCOConnectionType: Equatable, Hashable { } } - static func parseConnType(msg: String) -> MCOConnectionType?{ + static func parseConnType(msg: String) -> MCOConnectionType? { /* RFC 3501 (IMAP): See: https://tools.ietf.org/html/rfc3501#section-6.1.1 @@ -794,7 +924,7 @@ extension MCOConnectionType: Equatable, Hashable { if msg.contains("starttls".lowercased()) { return MCOConnectionType.startTLS } - else if msg.contains("tls".lowercased()) && msg.contains("CAPABILITY".lowercased()){ + else if msg.contains("tls".lowercased()) && msg.contains("CAPABILITY".lowercased()) { return MCOConnectionType.TLS } return nil @@ -842,10 +972,8 @@ extension MCOAuthType: Comparable { } static func parseAuthType(msg: String) -> [MCOAuthType] { - /* - RFC 3501 (IMAP): - See: https://tools.ietf.org/html/rfc3501#section-6.1.1 - */ + // RFC 3501 (IMAP): + // See: https://tools.ietf.org/html/rfc3501#section-6.1.1 let msg = msg.lowercased() var authTypes: [MCOAuthType] = [] if msg.contains("CAPABILITY".lowercased()) { // IMAP @@ -861,23 +989,23 @@ extension MCOAuthType: Comparable { if msg.contains("AUTH=GSSAPI".lowercased()) { authTypes.append(MCOAuthType.SASLGSSAPI) } - if msg.contains("AUTH=Kerberos".lowercased()){ + if msg.contains("AUTH=Kerberos".lowercased()) { authTypes.append(MCOAuthType.saslKerberosV4) } - if msg.contains("AUTH=LSRP".lowercased()){ + if msg.contains("AUTH=LSRP".lowercased()) { authTypes.append(MCOAuthType.SASLSRP) } - if msg.contains("AUTH=LNTLM".lowercased()){ + if msg.contains("AUTH=LNTLM".lowercased()) { authTypes.append(MCOAuthType.SASLNTLM) } - if msg.contains("AUTH=CRAMMD5".lowercased()){ + if msg.contains("AUTH=CRAMMD5".lowercased()) { authTypes.append(MCOAuthType.SASLCRAMMD5) } - if msg.contains("AUTH=DIGESTMD5".lowercased()){ + if msg.contains("AUTH=DIGESTMD5".lowercased()) { authTypes.append(MCOAuthType.SASLDIGESTMD5) } - } - else if msg.contains("250-AUTH".lowercased()) { // SMTP + } else if msg.contains("250-AUTH".lowercased()) { + // SMTP if msg.contains("XOAUTH2".lowercased()) { authTypes.append(MCOAuthType.xoAuth2) } @@ -890,19 +1018,19 @@ extension MCOAuthType: Comparable { if msg.contains("GSSAPI".lowercased()) { authTypes.append(MCOAuthType.SASLGSSAPI) } - if msg.contains("Kerberos".lowercased()){ + if msg.contains("Kerberos".lowercased()) { authTypes.append(MCOAuthType.saslKerberosV4) } - if msg.contains("LSRP".lowercased()){ + if msg.contains("LSRP".lowercased()) { authTypes.append(MCOAuthType.SASLSRP) } - if msg.contains("LNTLM".lowercased()){ + if msg.contains("LNTLM".lowercased()) { authTypes.append(MCOAuthType.SASLNTLM) } - if msg.contains("CRAMMD5".lowercased()){ + if msg.contains("CRAMMD5".lowercased()) { authTypes.append(MCOAuthType.SASLCRAMMD5) } - if msg.contains("DIGESTMD5".lowercased()){ + if msg.contains("DIGESTMD5".lowercased()) { authTypes.append(MCOAuthType.SASLDIGESTMD5) } } diff --git a/enzevalos_iphone/PGP/SwiftPGP.swift b/enzevalos_iphone/PGP/SwiftPGP.swift index 1a7ece176f1e3b3465659de64db06b3f9b839acb..64ad54fee0bf121bb9c5a1678c7b1c520bd3d53e 100644 --- a/enzevalos_iphone/PGP/SwiftPGP.swift +++ b/enzevalos_iphone/PGP/SwiftPGP.swift @@ -367,14 +367,14 @@ class SwiftPGP: Encryption { passcode = generatePW(size: PasscodeSize, splitInBlocks: true) } exportPwKeyChain[id] = passcode - if let message = armoredKey.data(using: .utf8) { - /* - if let cipher = try? ObjectivePGP.symmetricEncrypt(message, signWith: nil, encryptionKey: passcode, passphrase: passcode, armored: false){ - let armorMessage = Armor.armored(cipher, as: PGPArmorType.message) - return armorMessage - } - */ - } +// if let message = armoredKey.data(using: .utf8) { +// +// if let cipher = try? ObjectivePGP.symmetricEncrypt(message, signWith: nil, encryptionKey: passcode, passphrase: passcode, armored: false){ +// let armorMessage = Armor.armored(cipher, as: PGPArmorType.message) +// return armorMessage +// } +// +// } return nil } return armoredKey @@ -384,13 +384,20 @@ class SwiftPGP: Encryption { } func importKey(_ passcode: String, key: String) -> [String] { - if let keyData = try? Armor.readArmored(key) { - /* - if let plaintext = try? ObjectivePGP.symmetricDecrypt(keyData, key: passcode, verifyWith: nil, signed: nil, valid: nil, integrityProtected: nil), let ids = try? importKeys(data: plaintext, pw: nil, secret: true) { - return ids - } - */ - } +// if let keyData = try? Armor.readArmored(key) { +// if let plaintext = try? ObjectivePGP +// .symmetricDecrypt(keyData, +// key: passcode, +// verifyWith: nil, +// signed: nil, +// valid: nil, +// integrityProtected: nil), +// let ids = try? importKeys(data: plaintext, +// pw: nil, +// secret: true) { +// return ids +// } +// } return [] } @@ -817,39 +824,45 @@ class SwiftPGP: Encryption { if let p = password{ pw = p } - var chiphers = [String]() + let chiphers = [String]() - for text in textToEncrypt{ - if let data = text.data(using: .utf8){ - /* - if let chipher = try? ObjectivePGP.symmetricEncrypt(data, signWith: nil, encryptionKey: password, passphrase: pw, armored: false){ - if armored{ - chiphers.append(Armor.armored(chipher, as: PGPArmorType.message)) - } - else{ - chiphers.append(chipher.base64EncodedString(options: .init(arrayLiteral: .lineLength76Characters, .endLineWithLineFeed))) - } - } - */ - } - } +// for text in textToEncrypt { +// if let data = text.data(using: .utf8) { +// if let chipher = try? ObjectivePGP +// .symmetricEncrypt(data, +// signWith: nil, +// encryptionKey: password, +// passphrase: pw, +// armored: false) { +// if armored { +// chiphers.append(Armor.armored(chipher, as: PGPArmorType.message)) +// } else { +// chiphers.append(chipher.base64EncodedString(options: .init(arrayLiteral: .lineLength76Characters, .endLineWithLineFeed))) +// } +// } +// } +// } return (chiphers, pw) } func symmetricDecrypt(chipherTexts: [String], password: String) -> [String]{ - var plaintexts = [String]() + let plaintexts = [String]() - for chipher in chipherTexts{ - if let data = chipher.data(using: .utf8){ - /* - if let plainData = try? ObjectivePGP.symmetricDecrypt(data, key: password, verifyWith: nil, signed: nil, valid: nil, integrityProtected: nil){ - if let plainText = String(data: plainData, encoding: .utf8){ - plaintexts.append(plainText) - } - } - */ - } - } +// for chipher in chipherTexts { +// if let data = chipher.data(using: .utf8) { +// if let plainData = try? ObjectivePGP +// .symmetricDecrypt(data, +// key: password, +// verifyWith: nil, +// signed: nil, +// valid: nil, +// integrityProtected: nil) { +// if let plainText = String(data: plainData, encoding: .utf8){ +// plaintexts.append(plainText) +// } +// } +// } +// } return plaintexts } } diff --git a/enzevalos_iphone/SwiftUI/Compose/ComposeHeaderView.swift b/enzevalos_iphone/SwiftUI/Compose/ComposeHeaderView.swift new file mode 100644 index 0000000000000000000000000000000000000000..1927a048194bbb60be8d831c04ec7abe6f2ff536 --- /dev/null +++ b/enzevalos_iphone/SwiftUI/Compose/ComposeHeaderView.swift @@ -0,0 +1,83 @@ +// +// ComposeHeaderView.swift +// enzevalos_iphone +// +// Created by Chris Offner & Claire Bräuer on 23.03.21. +// Copyright © 2021 fu-berlin. All rights reserved. +// + +import SwiftUI + +/// A view that contains the Cancel and Send buttons for an email. +struct ComposeViewHeader: View { + @Environment(\.presentationMode) var presentationMode + @ObservedObject var model: ComposeModel + + var body: some View { + ZStack { + HStack { + // Cancel button + Button("Cancel") { + presentationMode.wrappedValue.dismiss() + } + + Spacer() + + // Send button, disabled if no recipients are set. + Button("Send") { + model.sendMail() + presentationMode.wrappedValue.dismiss() + } + .disabled(model.recipientsModel.hasNoRecipients) + } + + // Encryption toggle + Toggle("", isOn: $model.encryptionOn) + .toggleStyle(EncryptionToggleStyle()) + .labelsHidden() + } + } + + /// Custom styling for the encryption toggle in ComposeViewHeader. + struct EncryptionToggleStyle: ToggleStyle { + func makeBody(configuration: Configuration) -> some View { + Button { + configuration.isOn.toggle() + } label: { + // Separate labels required for desired scaling transition between states + if configuration.isOn { + encryptedButtonLabel + } else { + unencryptedButtonLabel + } + } + .frame(width: 40, height: 40) + } + + /// Label style for encryption button when encryption is activated. + private var encryptedButtonLabel: some View { + ZStack { + Circle() + .stroke(Color.blue, lineWidth: 2) + + Image(systemName: "lock.fill") + .font(Font.system(size: 24, weight: Font.Weight.light)) + .foregroundColor(.blue) + } + .transition(AnyTransition.opacity.combined(with: .scale)) + } + + /// Label style for encryption button when encryption is deactivated. + private var unencryptedButtonLabel: some View { + ZStack { + Circle() + .fill(Color(UIColor.tertiaryLabel)) + + Image(systemName: "lock.slash.fill") + .font(Font.system(size: 24, weight: Font.Weight.light)) + .foregroundColor(.white) + } + .transition(AnyTransition.opacity.combined(with: .scale)) + } + } +} diff --git a/enzevalos_iphone/SwiftUI/Compose/ComposeModel.swift b/enzevalos_iphone/SwiftUI/Compose/ComposeModel.swift index 7d3dae27203df71f874f8d101bb9b18583442cb9..4a85b6453f12d8c9cb413a8144e78f5b19510138 100644 --- a/enzevalos_iphone/SwiftUI/Compose/ComposeModel.swift +++ b/enzevalos_iphone/SwiftUI/Compose/ComposeModel.swift @@ -3,6 +3,7 @@ // enzevalos_iphone // // Created by Oliver Wiese on 12.11.20. +// Modified by Chris Offner & Claire Bräuer in March 2021. // Copyright © 2020 fu-berlin. All rights reserved. // @@ -12,8 +13,8 @@ import Foundation class ComposeModel: ObservableObject { @Published var subject = "" @Published var body = "" - @Published var encryptionOff = false - var recipientsModel: RecipientsModel = RecipientsModel() + @Published var encryptionOn = true + @Published var recipientsModel: RecipientsModel = RecipientsModel() init(preData: PreMailData?) { if let preData = preData { @@ -23,9 +24,10 @@ class ComposeModel: ObservableObject { addAddresses(preData.cc, model: recipientsModel.ccModel) addAddresses(preData.bcc, model: recipientsModel.bccModel) } + recipientsModel.parentComposeModel = self } - // TODO: Add security state + // TODO: Add security state functionality /// Generates mail and sends it. func sendMail() { @@ -34,8 +36,9 @@ class ComposeModel: ObservableObject { /// Adds email addresses to given RecipientFieldModel. /// - /// - Parameter addresses: String array of email addresses to add. - /// - Parameter model: RecipientFieldModel to which to add the addresses. + /// - Parameters: + /// - addresses: String array of email addresses to add. + /// - model: RecipientFieldModel to which to add the addresses. private func addAddresses(_ addresses: [String], model: RecipientFieldModel) { for address in addresses { model.addNewAddress(address) @@ -43,6 +46,8 @@ class ComposeModel: ObservableObject { } /// Generates OutgoingMail with given email contents. + /// + /// - Returns: Outgoing mail filled out with relevant information from RecipientsModel. private func generateMail() -> OutgoingMail { OutgoingMail(toAddresses: recipientsModel.toEMails, ccAddresses: recipientsModel.ccEMails, @@ -51,7 +56,7 @@ class ComposeModel: ObservableObject { textContent: body, htmlContent: nil, textparts: 1, - sendEncryptedIfPossible: !encryptionOff, + sendEncryptedIfPossible: encryptionOn, attachments: []) } } diff --git a/enzevalos_iphone/SwiftUI/Compose/ComposeView.swift b/enzevalos_iphone/SwiftUI/Compose/ComposeView.swift index 9592800013bf53993b6eea605ac30c73b111c334..937ceb3fd6d33f013050ec9f272a0d1df42db7a5 100644 --- a/enzevalos_iphone/SwiftUI/Compose/ComposeView.swift +++ b/enzevalos_iphone/SwiftUI/Compose/ComposeView.swift @@ -10,190 +10,206 @@ import SwiftUI /// A view used to compose and send an email. struct ComposeView: View { @State private var cc = "" - @State private var isEditingCcOrBcc = false - @ObservedObject var composer: ComposeModel + @ObservedObject var model: ComposeModel /// - Parameter preData: Data of email to reply to or forward. init(preData: PreMailData? = nil) { - composer = ComposeModel(preData: preData) + model = ComposeModel(preData: preData) } var body: some View { - VStack { - // Top bar with Cancel and Send button - ComposeViewHeader() - .environmentObject(composer) - - Divider() - - // "To" recipient - RecipientField(recipientFieldModel: composer.recipientsModel.toModel) - Divider() - - // "Cc/Bcc" recipient - if isEditingCcOrBcc || !cc.isEmpty { - RecipientField(recipientFieldModel: composer.recipientsModel.ccModel) + GeometryReader { geometry in + VStack { + // Top bar with Cancel and Send button + ComposeViewHeader(model: model) + + Divider() + + // "To" recipients + RecipientField(model: model.recipientsModel.toModel, + composeViewHeight: geometry.size.height) Divider() - RecipientField(recipientFieldModel: composer.recipientsModel.bccModel) - } else { - // TODO: Solve "Cc/Bcc" <--> "Cc", "Bcc" switch more elegantly. - // "Cc/Bcc" field never actually gets used, it gets replaced by - // separate "Cc" and "Bcc" RecipientFields above, once selected. - TextField("Cc" + "/" + "Bcc", text: $cc) { ccSelected in - self.isEditingCcOrBcc = ccSelected + // "Cc/Bcc" recipients + CcAndBccFields(model: model.recipientsModel, + composeViewHeight: geometry.size.height) + + // Subject + HStack { + Text("Subject") + .foregroundColor(Color(UIColor.tertiaryLabel)) + TextField("", text: $model.subject) + .autocapitalization(.none) } + + Divider() + + // Email body + TextEditor(text: $model.body) } - - Divider() - - // Subject - HStack { - Text("Subject") - .foregroundColor(Color(UIColor.tertiaryLabel)) - TextField("", text: $composer.subject) - .autocapitalization(.none) - .frame(minWidth: 0, maxWidth: .infinity) - } - Divider() - - // Email body - TextEditor(text: $composer.body) + .padding() + .animation(.easeInOut) + .ignoresSafeArea(edges: /*@START_MENU_TOKEN@*/.bottom/*@END_MENU_TOKEN@*/) } - .padding() - .animation(.default) - Spacer() } } /// A view in which recipients get added or removed. struct RecipientField: View { - @ObservedObject var recipientFieldModel: RecipientFieldModel - @State var showList = false + @ObservedObject var model: RecipientFieldModel + @State private var showList = false + @State private var indexOfSelected: Int? + var composeViewHeight: CGFloat var body: some View { VStack { HStack { // Recipient text field - Text(recipientFieldModel.type.asString) + Text(model.type.rawValue) .foregroundColor(Color(UIColor.tertiaryLabel)) - // Shows selected recipients as blue capsules - // followed by TextField for new recipients + // Selected recipients as blue capsules, + // followed by TextField for new recipients. ScrollView(.horizontal) { - HStack(spacing: 4) { - ForEach(recipientFieldModel.selectedContacts) { (recipient: AddressRecord) in - Text(recipient.displayname ?? recipient.email) - .foregroundColor(.white) - .padding(.horizontal, 12) - .padding(.vertical, 2) - .background(Color.accentColor) - .clipShape(Capsule()) - } - TextField("", text: $recipientFieldModel.text, onCommit: { - recipientFieldModel.commitText() - }) - .frame(minWidth: 200) - .autocapitalization(.none) - .keyboardType(.emailAddress) + HStack(spacing: 3) { + CommittedRecipients + NewRecipientTextField } } // Toggles contact list - Button(action: { showList.toggle() }) { - Image(systemName: !showList ? "plus.circle" : "chevron.up.circle") + if model.type != .ccBcc { + Button(action: { showList.toggle() }) { + Image(systemName: !showList ? "plus.circle" : "chevron.up.circle") + } } } .frame(height: 20) // Contact list - if showList { + if showList || !model.suggestions.isEmpty { Divider() RecipientListView() - .frame(height: 464) + .environmentObject(model) + .frame(height: composeViewHeight * 0.55) + } + } + } + + /// A view that lists all recipients that have been committed to this RecipientField. + var CommittedRecipients: some View { + ForEach(model.selectedContacts) { (recipient: AddressRecord) in + let isSelected = model.selectedContacts.firstIndex(of: recipient) == indexOfSelected + + RecipientCapsule(recipient: recipient, + isSelected: isSelected, + model: model, + indexOfSelected: $indexOfSelected) + .onTapGesture { + indexOfSelected = isSelected ? nil : model.selectedContacts + .firstIndex(of: recipient) + } + } + .padding(2) + } + + /// A TextField into which new recipients can be entered. + var NewRecipientTextField: some View { + TextField("", text: $model.text) { isEditing in + if model.type != .to, + let parent = model.parentRecipientModel { + parent.isEditingCcOrBcc = isEditing } + + } onCommit: { + model.commitText() + // TODO: Fix bug on first Cc or Bcc recipient commit + // For some reason, model.selectedContacts.count stays 0 + // after the first committed recipient, leading to the Cc + // and Bcc fields getting collapsed again despite the + // first recipient clearly getting rendered as a blue + // capsule in the ForEach loop above. 🤔🤔🤔 + print(model.selectedContacts.count) } - .environmentObject(recipientFieldModel) + .frame(width: 200) // TODO: Stretch dynamically over available horizontal space + .autocapitalization(.none) // .frame(maxWidth: .infinity) somehow doesn't work + .keyboardType(.emailAddress) } } -/// A view containing the Cancel and Send buttons for an email. -struct ComposeViewHeader: View { - @Environment(\.presentationMode) var presentationMode - @EnvironmentObject var composer: ComposeModel - +/// A view that shows either a single "Cc/Bcc" field or separate "Cc" and "Bcc" fields. +struct CcAndBccFields: View { + @ObservedObject var model: RecipientsModel + var composeViewHeight: CGFloat + var body: some View { VStack { - // Grab handle in top center - Capsule() - .fill(Color(.lightGray)) - .frame(width: 70, height: 5) + RecipientField(model: model.ccModel, + composeViewHeight: composeViewHeight) + Divider() - HStack { - // Cancel button - Button("Cancel") { - presentationMode.wrappedValue.dismiss() - } - - Spacer() - - // Send button - Button(action: { - composer.sendMail() - presentationMode.wrappedValue.dismiss() - }) { - if composer.encryptionOff { - UnencryptedSendButton(grayedOut: composer.recipientsModel.hasNoRecipients) - } else { - EncryptedSendButton(grayedOut: composer.recipientsModel.hasNoRecipients) - } - } - .disabled(composer.recipientsModel.hasNoRecipients) + if model.showBccField { + RecipientField(model: model.bccModel, + composeViewHeight: composeViewHeight) + Divider() } } } } -/// Styling for Send button if email gets sent encrypted. -struct EncryptedSendButton: View { - var grayedOut: Bool +/// A view that shows a recipient committed to a recipient field. +struct RecipientCapsule: View { + var recipient: AddressRecord + var isSelected: Bool + @ObservedObject var model: RecipientFieldModel + @Binding var indexOfSelected: Int? var body: some View { - HStack { - Image(systemName: "lock") - .foregroundColor(Color.white) - Spacer() - Text("Send") - .foregroundColor(Color.white) + ZStack { + Capsule() + .stroke(isSelected ? Color.accentColor : .secondary, + lineWidth: isSelected ? 2 : 1) + + HStack { + Text(name) + .fixedSize(horizontal: true, vertical: false) + + // Remove button shown if recipient capsule is selected + if isSelected { + Button(action: removeRecipient) { + Image(systemName: "xmark") + } // Transition: Slide in from left, slide out to left + .transition(.asymmetric( + insertion: AnyTransition + .opacity.combined(with: .slide), + removal: AnyTransition + .opacity.combined(with: .move(edge: .leading))) + ) + } + } + .padding(EdgeInsets(top: 2, leading: 8, bottom: 2, trailing: 8)) + .foregroundColor(isSelected ? .accentColor : .secondary) } - .padding(.horizontal) - .padding(.vertical, 5) - .background(Capsule().fill(grayedOut ? Color(UIColor.tertiaryLabel) : Color.blue)) - .frame(width: 101, height: 40) } -} - -/// Styling for Send button if email gets sent unencrypted. -struct UnencryptedSendButton: View { - var grayedOut: Bool - var body: some View { - HStack { - Image(systemName: "lock.open") - .foregroundColor(Color.blue) - Spacer() - Text("Send") - .foregroundColor(Color.blue) + // TODO: Use actual displayname once it's implemented. + // Currently just uses first part of email before "@". + var name: String { + recipient.displayname ?? recipient.email.components(separatedBy: "@")[0] + } + + /// Removes recipient from selected contacts of respective RecipientFieldModel. + func removeRecipient() { + if let index = model.selectedContacts.firstIndex(of: recipient) { + model.deselectContact(at: index) + indexOfSelected = nil } - .padding(.horizontal) - .padding(.vertical, 5) - .background(Capsule().stroke()) - .frame(width: 101, height: 40) } } +/// Canvas Preview struct ComposeView_Previews: PreviewProvider { static var previews: some View { ComposeView() diff --git a/enzevalos_iphone/SwiftUI/Compose/RecipientFieldModel.swift b/enzevalos_iphone/SwiftUI/Compose/RecipientFieldModel.swift index 34cba3ee041c238f0355e6dea1a1607580f9766b..2979c8213f42105a4c4cd9af1e2ff282973f36ab 100644 --- a/enzevalos_iphone/SwiftUI/Compose/RecipientFieldModel.swift +++ b/enzevalos_iphone/SwiftUI/Compose/RecipientFieldModel.swift @@ -3,6 +3,7 @@ // enzevalos_iphone // // Created by Oliver Wiese on 31.10.20. +// Modified by Chris Offner & Claire Bräuer in March 2021. // Copyright © 2020 fu-berlin. All rights reserved. // @@ -11,18 +12,19 @@ import SwiftUI /// A model for a single recipient field. class RecipientFieldModel: ObservableObject { - @Published var suggestions: [AddressRecord] = [] - @Published var selectedContacts = [AddressRecord]() // TODO: As AddressProperty??? - private var dataprovider: PersistentDataProvider - let type: RecipientType + @Published var suggestions = [AddressRecord]() + @Published var selectedContacts = [AddressRecord]() + @Published var type: RecipientType + private var dataprovider: PersistentDataProvider = LetterboxModel.instance.dataProvider + var parentRecipientModel: RecipientsModel? - init(type: RecipientType, parent: RecipientsModel?, dataprovider: PersistentDataProvider) { + init(type: RecipientType) { self.type = type - self.dataprovider = dataprovider } @Published var text = "" { didSet { + // Split entered text at separator characters, add first part as address if text.contains(" ") || text.contains(",") || text.contains(";") { var firstWord = text.split(separator: "\n").first firstWord?.removeLast() @@ -32,34 +34,61 @@ class RecipientFieldModel: ObservableObject { text = "" } - // TODO: Show suggestions in ComposeView. + // Generate suggestions based on entered text. if !text.isEmpty { - let frc = dataprovider.createFetchResultController(fetchRequest: AddressRecord.lookForPrefix(prefix: text)) - if let addresses = frc.fetchedObjects { - suggestions = addresses - } else { - suggestions = [] - } + // Fetch addresses that match entered prefix. + let frc = dataprovider.createFetchResultController( + fetchRequest: AddressRecord.lookForPrefix(prefix: text) + ) + + // Set addresses as suggestions if any were found, otherwise empty suggestions + suggestions = frc.fetchedObjects ?? [] } else { suggestions = [] } } } - /// Adds contact at index to recipients. - func selectContact(addr: AddressRecord) { - selectedContacts.append(addr) + /// Gets array of contacts sorted by name or recency. + /// + /// - Parameter sortBy: Determines what key the contacts get sorted by. + /// - Returns: Sorted array of contacts. + func getContacts(sortBy: AddressRecord.SortBy) -> [AddressRecord] { + if !suggestions.isEmpty { + return suggestions + } + + let frc = LetterboxModel.instance.dataProvider.createFetchResultController( + fetchRequest: AddressRecord.all(sortBy: sortBy) + ) + + return frc.fetchedObjects ?? [] + } + + /// Adds AddressRecord of a contact to array of recipients. + /// + /// - Parameter _: AddressRecord that gets added to array of recipients. + func selectContact(_ addressRecord: AddressRecord) { + selectedContacts.append(addressRecord) text = "" suggestions = [] + + // TODO: Find better way to manage state and UI updates! + // This is a horrible hack to get UI to refresh for Send button enable/disable state. + // Please fix wherever you find the following line of code. May require architecture + // changes because state changes (currently ) don't propagate through nested models. + // Read: https://rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui/ + parentRecipientModel?.parentComposeModel?.subject += "" } /// Removes contact at index from recipients. - func unselectContactAt(i: Int) { - if i < selectedContacts.count { - selectedContacts.remove(at: i) - } else { - print("ERROR wrong index! \(i) but \(selectedContacts.count)") - } + /// + /// - Parameter at: Index of contact to be removed from array of recipients. + func deselectContact(at index: Int) { + selectedContacts.remove(at: index) + + // TODO: See TODO in selectContact. + parentRecipientModel?.parentComposeModel?.subject += "" } /// Commits text to addresses. @@ -68,16 +97,27 @@ class RecipientFieldModel: ObservableObject { addNewAddress(text) text = "" } + + // TODO: See TODO in selectContact. + parentRecipientModel?.parentComposeModel?.subject += "" } + /// Adds new address to selected contacts. + /// + /// - Parameter _: Email address string to be added. func addNewAddress(_ address: String) { - guard address.count > 0 else { - // TODO: Add email validiation + warning? + guard address.count > 0 && address.contains("@") else { + // TODO: Add proper email validation + warning return } dataprovider.importNewData(from: [AddressProperties(email: address)]) { error in - let frc = self.dataprovider.createFetchResultController(fetchRequest: AddressRecord.lookForExisting(email: address)) + let frc = self + .dataprovider + .createFetchResultController( + fetchRequest: AddressRecord.lookForExisting(email: address) + ) + if let addresses = frc.fetchedObjects, let addr = addresses.first { self.selectedContacts.append(addr) } diff --git a/enzevalos_iphone/SwiftUI/Compose/RecipientListView.swift b/enzevalos_iphone/SwiftUI/Compose/RecipientListView.swift index db3c21a6e428ceb04ec87cd9a9a56ce1a0d1e399..6663d18655be20d6bded7433e67588f40309fa25 100644 --- a/enzevalos_iphone/SwiftUI/Compose/RecipientListView.swift +++ b/enzevalos_iphone/SwiftUI/Compose/RecipientListView.swift @@ -10,34 +10,25 @@ import SwiftUI /// A view that lists contacts for selection as recipients. struct RecipientListView: View { @EnvironmentObject var model: RecipientFieldModel - @State var sortByName = true - - // Array of contacts sorted by name or recency. - var contacts: [AddressRecord] { - let frc = LetterboxModel.instance.dataProvider.createFetchResultController(fetchRequest: AddressRecord.all(sortBy: sortByName ? .name : .recency)) - if let addresses = frc.fetchedObjects { - return addresses - } - return [] - } + @State private var sortByName = true var body: some View { VStack(alignment: .leading) { // Sorting options HStack { - Text("Sort by:") + Text("SortBy") .foregroundColor(.secondary) - Button(sortByName ? "Name" : "Last Contacted") { + Button(sortByName ? "Name" : "LastContacted") { sortByName.toggle() } } .font(.caption) .transition(.opacity) - .id(sortByName ? "Name" : "Last Contacted") + .id(sortByName ? "Name" : "LastContacted") // Contact list ScrollView { - ForEach(contacts) { contact in + ForEach(model.getContacts(sortBy: sortByName ? .name : .recency)) { contact in RecipientRowView(contact: contact) Divider() } diff --git a/enzevalos_iphone/SwiftUI/Compose/RecipientRowView.swift b/enzevalos_iphone/SwiftUI/Compose/RecipientRowView.swift index 93b85acaaab43f1c3d09582ccec0dd480c4111cf..d09c63355bb6ed077e5268d068c4031b0c0d4ecc 100644 --- a/enzevalos_iphone/SwiftUI/Compose/RecipientRowView.swift +++ b/enzevalos_iphone/SwiftUI/Compose/RecipientRowView.swift @@ -13,48 +13,49 @@ struct RecipientRowView: View { @EnvironmentObject var model: RecipientFieldModel var body: some View { - HStack { - // Profile picture - contact.avatar - .resizable() - .frame(width: 30, height: 30) - .aspectRatio(contentMode: .fit) - .clipShape(/*@START_MENU_TOKEN@*/Circle()/*@END_MENU_TOKEN@*/) - .shadow(radius: 3) - .padding(.trailing, 8) - - VStack(alignment: .leading) { - HStack { - // TODO: Show proper displayname. - // Currently for debugging purposes we show the email - // before "@" as name because displaynames are all nil. - Text(contact.displayname ?? contact.email.components(separatedBy: "@")[0]) - Spacer() - Text(contact.lastHeardFrom) + Button { + if let index = model.selectedContacts.firstIndex(where: { $0.email == contact.email }) { + model.deselectContact(at: index) + } else { + model.selectContact(contact) + } + } label: { + HStack { + // Profile picture + contact.avatar + .resizable() + .frame(width: 30, height: 30) + .aspectRatio(contentMode: .fit) + .clipShape(/*@START_MENU_TOKEN@*/Circle()/*@END_MENU_TOKEN@*/) + .shadow(radius: 2) + .padding(EdgeInsets(top: 0, leading: 3, bottom: 0, trailing: 8)) + + VStack(alignment: .leading) { + HStack { + // TODO: Show proper displayname. + // Currently for debugging purposes we show the email + // before "@" as name because displaynames are all nil. + Text(contact.displayname ?? contact.email.components(separatedBy: "@")[0]) + .foregroundColor(.primary) + Spacer() + Text(contact.lastHeardFrom) + .font(.caption) + .padding(.trailing, 16) + } + + // Currently only shows first email address of contact. + // TODO: Decide which email address(es) to show. + Text(contact.email) .font(.caption) - .foregroundColor(.secondary) - .padding(.trailing, 16) } + .foregroundColor(.secondary) + + Spacer() - // Currently only shows first email address of contact. - // TODO: Decide which email address(es) to show. - Text(contact.email) - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - - // Adds or removes contact from recipients. - Button(action: { - if let index = model.selectedContacts.firstIndex(where: { $0.email == contact.email }) { - model.unselectContactAt(i: index) - } else { - model.selectContact(addr: contact) - } - }) { // TODO: Maybe use more robust identifier (Identifiable + UUID?) - Image(systemName: model.selectedContacts.contains(where: { $0.email == contact.email }) ? "checkmark.circle.fill" : "circle") + Image(systemName: model.selectedContacts.contains { + $0.email == contact.email + } ? "checkmark.circle.fill" : "circle") .foregroundColor(.accentColor) } } diff --git a/enzevalos_iphone/SwiftUI/Compose/RecipientsModel.swift b/enzevalos_iphone/SwiftUI/Compose/RecipientsModel.swift index 7003e831ba947eb8d717b01b7fe3c2e93f62766d..1e2c54290c9a45c60fd369f14bc32b89ef54f7f0 100644 --- a/enzevalos_iphone/SwiftUI/Compose/RecipientsModel.swift +++ b/enzevalos_iphone/SwiftUI/Compose/RecipientsModel.swift @@ -3,6 +3,7 @@ // enzevalos_iphone // // Created by Oliver Wiese on 09.11.20. +// Modified by Chris Offner & Claire Bräuer in March 2021. // Copyright © 2020 fu-berlin. All rights reserved. // @@ -10,63 +11,80 @@ import Foundation import Combine import SwiftUI -/// Type of recipient field (to, cc, bcc). -enum RecipientType { - case to - case cc - case bcc - - var asString: LocalizedStringKey { - switch self { - case .to: - return "To" - case .cc: - return "Cc" - case .bcc: - return "Bcc" - } - } -} - /// Holds models for "To", "Cc", and "Bcc" fields. class RecipientsModel: ObservableObject { let toModel: RecipientFieldModel let ccModel: RecipientFieldModel let bccModel: RecipientFieldModel + @Published var showBccField = false + var parentComposeModel: ComposeModel? /// Initializes models for "To", "Cc", and "Bcc" fields. init() { - let dataprovider = LetterboxModel.instance.dataProvider - toModel = RecipientFieldModel(type: .to, parent: nil, dataprovider: dataprovider) - ccModel = RecipientFieldModel(type: .cc, parent: nil, dataprovider: dataprovider) - bccModel = RecipientFieldModel(type: .bcc, parent: nil, dataprovider: dataprovider) + toModel = RecipientFieldModel(type: .to) + ccModel = RecipientFieldModel(type: .ccBcc) + bccModel = RecipientFieldModel(type: .bcc) + toModel.parentRecipientModel = self + ccModel.parentRecipientModel = self + bccModel.parentRecipientModel = self + } + + /// Used to show or hide Bcc field + var isEditingCcOrBcc: Bool = false { + didSet { + print("isEditingCcOrBcc: \(isEditingCcOrBcc)") + updateShowBccField() + print("showBccField: \(showBccField)") + } } /// Used to deactivate Send button if email has no recipients. var hasNoRecipients: Bool { - toModel.selectedContacts.isEmpty && - ccModel.selectedContacts.isEmpty && - bccModel.selectedContacts.isEmpty + toModel.selectedContacts.isEmpty + && ccModel.selectedContacts.isEmpty + && bccModel.selectedContacts.isEmpty } /// String array of email addresses in "To" field. var toEMails: [String] { get { - toModel.selectedContacts.map({$0.email}) + toModel.selectedContacts.map { + $0.email + } } } /// String array of email addresses in "Cc" field. var ccEMails: [String] { get { - ccModel.selectedContacts.map({$0.email}) + ccModel.selectedContacts.map { + $0.email + } } } /// String array of email addresses in "Bcc" field. var bccEMails: [String] { get { - bccModel.selectedContacts.map({$0.email}) + bccModel.selectedContacts.map { + $0.email + } } } + + /// Updates whether to display "Bcc" field based on state of recipient fields. + func updateShowBccField() { + showBccField = isEditingCcOrBcc + || !ccModel.selectedContacts.isEmpty + || !bccModel.selectedContacts.isEmpty + ccModel.type = showBccField ? .cc : .ccBcc + } +} + +/// Type of recipient field (to, cc, bcc). +enum RecipientType: LocalizedStringKey { + case to = "To" + case cc = "Cc" + case bcc = "Bcc" + case ccBcc = "CcBcc" } diff --git a/enzevalos_iphone/SwiftUI/DisplayProtocols.swift b/enzevalos_iphone/SwiftUI/DisplayProtocols.swift index 65b5181471b5854798b82dd4749c6ff078ccf6f1..59ddc9046b574349d7eea04fd18bcb1f365904cc 100644 --- a/enzevalos_iphone/SwiftUI/DisplayProtocols.swift +++ b/enzevalos_iphone/SwiftUI/DisplayProtocols.swift @@ -76,19 +76,15 @@ enum ContactSecurityRating { var name: LocalizedStringKey { switch self { - case .Trustworthy: - return "ContactView.Rating.Trustworthy" - case .Forgable: - return "ContactView.Rating.Forgable" + case .Trustworthy: return "ContactView.Rating.Trustworthy" + case .Forgable: return "ContactView.Rating.Forgable" } } var color: Color { switch self { - case .Trustworthy: - return Color(ThemeManager.encryptedVerifiedMessageColor()) - case .Forgable: - return Color(ThemeManager.unencryptedMessageColor()) + case .Trustworthy: return Color(ThemeManager.encryptedVerifiedMessageColor()) + case .Forgable: return Color(ThemeManager.unencryptedMessageColor()) } } } @@ -100,10 +96,8 @@ protocol DisplayKey { var lastSeen: Date? { get } var type: CryptoScheme { get } var isPreferedKey: Bool { get } - var signedMailsCounter: Int { get } var sentMailsCounter: Int { get } - } protocol DisplayFolder { @@ -250,7 +244,6 @@ extension DisplayMail { } } - var dialog: DialogStruct { get { // TODO: Do we add new public key stuff? @@ -414,7 +407,6 @@ extension DisplayMail { case (.ValidedEncryptedWithCurrentKey, .NoSignature): return .EncNoSignature case (.ValidedEncryptedWithCurrentKey, .NoPublicKey): return .EncButMissingPublicKeyToVerify } - } diff --git a/enzevalos_iphone/SwiftUI/FolderView/FolderListView.swift b/enzevalos_iphone/SwiftUI/FolderView/FolderListView.swift new file mode 100644 index 0000000000000000000000000000000000000000..aabf01383b0a8350da744ef84cbd2723e137636b --- /dev/null +++ b/enzevalos_iphone/SwiftUI/FolderView/FolderListView.swift @@ -0,0 +1,54 @@ +// +// FolderOverView.swift +// enzevalos_iphone +// +// Created by Oliver Wiese on 30.10.20. +// Modified by Chris Offner & Claire Bräuer in March 2021. +// Copyright © 2020 fu-berlin. All rights reserved. +// + +import SwiftUI + +/// A view that lists all email folders and lets the user navigate to them. +struct FolderListView: View { + @Environment(\.managedObjectContext) var managedObjectContext + @FetchRequest(fetchRequest: FolderRecord.FetchRequest) + private var folders: FetchedResults<FolderRecord> + @State private var nameOfChosenFolder: String? + + var body: some View { + NavigationView { + List(folders, id: \.self) { f in + let name = formatName(f.name) + + NavigationLink(destination: InboxView(folderPath: f.path ?? name, name: name) + .environment(\.managedObjectContext, + PersistentDataProvider + .dataProvider + .persistentContainer + .viewContext), + tag: f.name, + selection: $nameOfChosenFolder, + label: { FolderRowView(folder: f) }) + } + .navigationBarTitle("Folders", displayMode: .large) + } + .onAppear { nameOfChosenFolder = "INBOX" } + } + + /// Capitalizes first letter, lowercases all subsequent letters of string. + /// + /// - Parameter name: String to be formatted + /// - Returns: Formatted string + private func formatName(_ name: String) -> String { + name.prefix(1).capitalized + name.dropFirst().lowercased() + } +} + +// Preview +struct FolderOverView_Previews: PreviewProvider { + static var previews: some View { + FolderListView() + } +} + diff --git a/enzevalos_iphone/SwiftUI/FolderView/FolderOverView.swift b/enzevalos_iphone/SwiftUI/FolderView/FolderOverView.swift deleted file mode 100644 index 68b61ae2e41e06d78d86d3f1ad3028f17689a8a2..0000000000000000000000000000000000000000 --- a/enzevalos_iphone/SwiftUI/FolderView/FolderOverView.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// FolderOverView.swift -// enzevalos_iphone -// -// Created by Oliver Wiese on 30.10.20. -// Copyright © 2020 fu-berlin. All rights reserved. -// - -import SwiftUI - -struct FolderOverView: View { - @Environment(\.managedObjectContext) var managedObjectContext - @FetchRequest(fetchRequest: FolderRecord.FetchRequest) - var folders: FetchedResults<FolderRecord> - - - var body: some View { - List (folders, id: \.self){ f in - NavigationLink( - destination: MailListView(folderpath: f.path ?? f.name, name: f.name) - .environment(\.managedObjectContext, PersistentDataProvider.dataProvider.persistentContainer.viewContext) - ) { - FolderRow(folder: f) - } - } - .onAppear(perform: { - LetterboxModel.instance.mailHandler.allFolders({e in print("All folders: \(String(describing: e))")}) - }) - .navigationBarTitle("Folders", displayMode: .large) - } -} - -struct FolderOverView_Previews: PreviewProvider { - - static var previews: some View { - FolderOverView() - } -} - diff --git a/enzevalos_iphone/SwiftUI/FolderView/FolderRow.swift b/enzevalos_iphone/SwiftUI/FolderView/FolderRowView.swift similarity index 78% rename from enzevalos_iphone/SwiftUI/FolderView/FolderRow.swift rename to enzevalos_iphone/SwiftUI/FolderView/FolderRowView.swift index 406ab81d9026ef42a809bdb8d8a62f146f5b5f7d..557b31f70a9e0dccdd1b693ddc780d36f133cbc4 100644 --- a/enzevalos_iphone/SwiftUI/FolderView/FolderRow.swift +++ b/enzevalos_iphone/SwiftUI/FolderView/FolderRowView.swift @@ -8,14 +8,13 @@ import SwiftUI -struct FolderRow: View { +struct FolderRowView: View { //TODO: increase padding for each delimiter? -> Subfolder level? public var folder: DisplayFolder var body: some View { HStack { - folder.icon - .foregroundColor(.blue) + folder.icon.foregroundColor(.accentColor) Text(folder.name) Spacer() Text("\(folder.mails)") @@ -26,6 +25,6 @@ struct FolderRow: View { struct FolderRow_Previews: PreviewProvider { static var previews: some View { - FolderRow(folder: ProxyFolder(name: "Inbox")) + FolderRowView(folder: ProxyFolder(name: "Inbox")) } } diff --git a/enzevalos_iphone/SwiftUI/Inbox/InboxView.swift b/enzevalos_iphone/SwiftUI/Inbox/InboxView.swift index 21718661e2173dee237c91668cc9674117541859..421e01e0462e52339a34d84d2e89202bd64e3f0c 100644 --- a/enzevalos_iphone/SwiftUI/Inbox/InboxView.swift +++ b/enzevalos_iphone/SwiftUI/Inbox/InboxView.swift @@ -3,58 +3,126 @@ // enzevalos_iphone // // Created by Oliver Wiese on 16.10.20. +// Modified by Chris Offner & Claire Bräuer in March 2021. // Copyright © 2020 fu-berlin. All rights reserved. // import SwiftUI import CoreData // TODO: Refactor to Model! -// Updating text -> Last update, updating, no connection.... +// Updating text -> Last update, updating, no connection... struct InboxView: View { - var folderpath: String + var folderPath: String var name: String + @State private var updating = false + @State private var composeMail = false + @State private var goToFolders = false - @State var goToFolders = false - var body: some View { - NavigationView{ - MailListView(folderpath: folderpath, name: name) - .environment(\.managedObjectContext, PersistentDataProvider.dataProvider.persistentContainer.viewContext) - .navigationBarItems(leading: self.folderButton, trailing: keyManagementButton) + mailListView + .onAppear(perform: updateMails) + .sheet(isPresented: $composeMail) { ComposeView() } + .navigationBarItems(trailing: keyManagementButton) + .toolbar { + ToolbarItem(placement: .status) { + lastUpdate + } + + ToolbarItemGroup(placement: .bottomBar) { + Spacer() + composeButton + } + } + } + + private var mailListView: some View { + MailListView(folderPath: folderPath, folderName: name) + .environment(\.managedObjectContext, + PersistentDataProvider + .dataProvider + .persistentContainer + .viewContext) + } + + private var composeButton: some View { + Button { + composeMail = true + } label: { + Image(systemName: "square.and.pencil").imageScale(.large) } } private var keyManagementButton: some View { - NavigationLink( - destination: KeyManagementOverview(), - label: { - Image(systemName: "slider.horizontal.3").imageScale(.large) - }) + NavigationLink(destination: KeyManagementOverview()) { + Image(systemName: "key") + .imageScale(.large) + .rotationEffect(Angle(degrees: 45)) + } } + // TODO: Refactor lastUpdate to model + private var lastUpdate: some View { + var text = NSLocalizedString("Updating", comment: "updating...") + + if !updating { + let last = Date() + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale.current + dateFormatter.timeStyle = .medium + let dateString = dateFormatter.string(from: last) + text = NSLocalizedString("LastUpdate", comment: "") + " " + dateString + } + + return Button(action: updateMails) { + Text(text).font(.callout) + } + } + + + // TODO: Refactor updateMails to model + private func updateMails() { + guard !updating else { + return + } + + LetterboxModel + .instance + .mailHandler + .updateFolder(folderpath: folderPath) { error in + if error == nil { + self.updating = false + } + // TODO: Add error message + } + updating = true + } + + private var folderButton: some View { - Button(action:{ + Button { goToFolders = true - }, label: { + } label: { Image(systemName: "tray.2").imageScale(.large) - }) - .background( - NavigationLink(destination: FolderOverView() - .environment(\.managedObjectContext, PersistentDataProvider.dataProvider.persistentContainer.viewContext), isActive: $goToFolders) { - EmptyView() - } - .hidden() - ) + } + .background(NavigationLink(destination: FolderListView() + .environment(\.managedObjectContext, + PersistentDataProvider + .dataProvider + .persistentContainer + .viewContext), + isActive: $goToFolders) {}) } - } - - +// Preview struct InboxView_Previews: PreviewProvider { static var previews: some View { - return InboxView(folderpath: "INBOX", name: "INBOX") - .environment(\.managedObjectContext, PersistentDataProvider.proxyPersistentDataProvider.persistentContainer.viewContext) + return InboxView(folderPath: "INBOX", name: "INBOX") + .environment(\.managedObjectContext, + PersistentDataProvider + .proxyPersistentDataProvider + .persistentContainer + .viewContext) } } diff --git a/enzevalos_iphone/SwiftUI/Inbox/MailListView.swift b/enzevalos_iphone/SwiftUI/Inbox/MailListView.swift index a6abf0237ae3a968529a1878cdf479872af72adf..6ef156f83112fa561cc55f7faf5920f778092a56 100644 --- a/enzevalos_iphone/SwiftUI/Inbox/MailListView.swift +++ b/enzevalos_iphone/SwiftUI/Inbox/MailListView.swift @@ -3,6 +3,7 @@ // enzevalos_iphone // // Created by Oliver Wiese on 16.10.20. +// Modified by Chris Offner & Claire Bräuer in March 2021. // Copyright © 2020 fu-berlin. All rights reserved. // @@ -15,118 +16,64 @@ struct MailListView: View { @Environment(\.managedObjectContext) var managedObjectContext var fetchRequest: FetchRequest<MailRecord> var mails: FetchedResults<MailRecord>{fetchRequest.wrappedValue} + var folderPath: String + var folderName: String - var folderpath: String - var name: String + @State private var showUser = false + @State private var searchText = "" + @State private var searchType = SearchType.All - @State var updating = false - @State var showUser = false - @State var searchText = "" - @State var searchField = 0 - @State var searchNow = false - @State var composeMail = false - - init(folderpath: String, name: String) { - fetchRequest = MailRecord.mailsInFolderFetchRequest(folderpath: folderpath) - self.folderpath = folderpath - self.name = name + init(folderPath: String, folderName: String) { + fetchRequest = MailRecord.mailsInFolderFetchRequest(folderpath: folderPath) + self.folderPath = folderPath + self.folderName = folderName } + var body: some View { - mainView - .navigationBarTitle(name, displayMode: .inline) + VStack(alignment: .leading) { + SearchView(searchText: $searchText, searchType: $searchType) + .padding() - .onAppear(perform: { - self.updateMails() - }) - .sheet(isPresented: $composeMail) { - ComposeView() - } - } - - private var mainView: some View { - VStack (alignment: .leading){ - SearchView(searchText: $searchText, searchField: $searchField, searchNow: $searchNow) - .padding(6) mailList - .padding(-10) - // Toolbar - HStack { - self.idButton - Spacer() - self.lastUpdate - Spacer() - self.composeButton - } - .padding(6) } + .navigationBarTitle(folderName, displayMode: .inline) } private var mailList: some View { - List (self.mails.filter(filterKeyRecord), id: \.self){ record in - NavigationLink( - destination: ReadMainView(model: ReadModel(mail: record))) { - MailRow(mail: record) + List { + ForEach(self.mails.filter(filterKeyRecord), id: \.self) { email in + NavigationLink(destination: ReadMainView(model: ReadModel(mail: email))) { + MailRowView(mail: email) + } } - } - .resignKeyboardOnDragGesture() // hide keyboard when dragging - } - - private var idButton: some View { - Button(action: { print("Go to my id") }) { - Text(NSLocalizedString("KeyID", comment: "id")) - } - } - - private var composeButton: some View { - Button(action: { - composeMail = true - }, label: { - Image(systemName: "square.and.pencil").imageScale(.large) - }) - } - - private var lastUpdate: some View { - var text = NSLocalizedString("Updating", comment: "updating...") - if !updating { - let last = Date() - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale.current - dateFormatter.timeStyle = .medium - let dateString = dateFormatter.string(from: last) - text = NSLocalizedString("LastUpdate", comment: "") + " " + dateString - } - return Button(action: updateMails, label: {Text(text) - .font(.callout) - - }) - } - - func updateMails() { - guard !updating else { - return - } - LetterboxModel.instance.mailHandler.updateFolder(folderpath: folderpath, completionCallback: {error in - if error == nil { - self.updating = false + .onDelete {_ in + // TODO: Perform actual deletion of email } - // TODO: Add error message - }) - updating = true + } + .listStyle(PlainListStyle()) } func filterKeyRecord(keyRecord: MailRecord) -> Bool { - let searchType = SearchType.findType(i: searchField) - if self.searchText.isEmpty || self.searchText == NSLocalizedString("Searchbar.Title", comment: "Search") { + if self.searchText.isEmpty + || self.searchText == NSLocalizedString("Searchbar.Title", comment: "Search") { return true } + let query = self.searchText.lowercased() - if (searchType == .All || searchType == .Sender) && (containsSearchTerms(content: keyRecord.sender.displayname, searchText: query) || containsSearchTerms(content: keyRecord.sender.email, searchText: query)) { + if (searchType == .All || searchType == .Sender) + && (containsSearchTerms(content: keyRecord.sender.displayname, searchText: query) + || containsSearchTerms(content: keyRecord.sender.email, searchText: query)) { return true - } else if (searchType == .All || searchType == .Sender) && keyRecord.addresses.filter({containsSearchTerms(content: $0.email, searchText: query)}).count > 0 { + } else if (searchType == .All || searchType == .Sender) + && keyRecord.addresses.filter({ + containsSearchTerms(content: $0.email, + searchText: query) }).count > 0 { return true - } else if (searchType == .All || searchType == .Subject) && containsSearchTerms(content: keyRecord.subject, searchText: query){ + } else if (searchType == .All || searchType == .Subject) + && containsSearchTerms(content: keyRecord.subject, searchText: query) { return true - } else if (searchType == .All || searchType == .Body) && containsSearchTerms(content: keyRecord.body, searchText: query){ + } else if (searchType == .All || searchType == .Body) + && containsSearchTerms(content: keyRecord.body, searchText: query) { return true } return false @@ -135,7 +82,7 @@ struct MailListView: View { struct MailListView_Previews: PreviewProvider { static var previews: some View { - MailListView(folderpath: "INBOX", name: "INBOX") + MailListView(folderPath: "INBOX", folderName: "INBOX") .environment(\.managedObjectContext, PersistentDataProvider.proxyPersistentDataProvider.persistentContainer.viewContext) } } diff --git a/enzevalos_iphone/SwiftUI/LetterboxApp.swift b/enzevalos_iphone/SwiftUI/LetterboxApp.swift index 2f93ce7c6d7ef383f0341e69894cfabc64428a46..25870715d77b86cc62c85b6bfb7b574d6a413bfa 100644 --- a/enzevalos_iphone/SwiftUI/LetterboxApp.swift +++ b/enzevalos_iphone/SwiftUI/LetterboxApp.swift @@ -16,11 +16,10 @@ struct LetterboxApp: App { var body: some Scene { WindowGroup { if testing { - NavigationView{ + NavigationView { KeyManagementOverview() } - } - else { + } else { switch model.currentState { case .PERMISSIONS: PermissionRequestView() @@ -31,8 +30,12 @@ struct LetterboxApp: App { case .GENERATEKEYS: PermissionRequestView() // TODO Wait case .LAUNCHEDBEFORE: - InboxView(folderpath: MailHandler.INBOX, name: NSLocalizedString("Inbox", comment: "Inbox")) - .environment(\.managedObjectContext, PersistentDataProvider.dataProvider.persistentContainer.viewContext) + FolderListView() + .environment(\.managedObjectContext, + PersistentDataProvider + .dataProvider + .persistentContainer + .viewContext) } } } diff --git a/enzevalos_iphone/SwiftUI/LetterboxModel.swift b/enzevalos_iphone/SwiftUI/LetterboxModel.swift index b8322b6662d502b9fc0d37fb67979a77a9d5cc94..c73d113fd03a29e61d7411ab8938b4100004cb81 100644 --- a/enzevalos_iphone/SwiftUI/LetterboxModel.swift +++ b/enzevalos_iphone/SwiftUI/LetterboxModel.swift @@ -45,13 +45,13 @@ enum UIState { class LetterboxModel: ObservableObject { static var instance: LetterboxModel { get { - if let instance = currentIntstance { + if let instance = currentInstance { return instance } return LetterboxModel() } } - private static var currentIntstance: LetterboxModel? + private static var currentInstance: LetterboxModel? let mailHandler = MailHandler() let dataProvider = PersistentDataProvider.dataProvider @@ -82,20 +82,16 @@ class LetterboxModel: ObservableObject { if flags.contains(.reachable) == false { // The target host is not reachable. return .notReachable - } - else if flags.contains(.isWWAN) == true { + } else if flags.contains(.isWWAN) == true { // WWAN connections are OK if the calling application is using the CFNetwork APIs. return .reachableViaWWAN - } - else if flags.contains(.connectionRequired) == false { + } else if flags.contains(.connectionRequired) == false { // If the target host is reachable and no connection is required then we'll assume that you're on Wi-Fi... return .reachableViaWiFi - } - else if (flags.contains(.connectionOnDemand) == true || flags.contains(.connectionOnTraffic) == true) && flags.contains(.interventionRequired) == false { + } else if (flags.contains(.connectionOnDemand) == true || flags.contains(.connectionOnTraffic) == true) && flags.contains(.interventionRequired) == false { // The connection is on-demand (or on-traffic) if the calling application is using the CFSocketStream or higher APIs and no [user] intervention is needed return .reachableViaWiFi - } - else { + } else { return .notReachable } } @@ -132,7 +128,7 @@ class LetterboxModel: ObservableObject { UserDefaults.standard.register(defaults: ["Signature.Switch": false]) UserDefaults.standard.register(defaults: ["Signature.Text": "Verfasst mit Letterbox. Mehr Informationen: http://letterbox-app.org"]) } - LetterboxModel.currentIntstance = self + LetterboxModel.currentInstance = self } diff --git a/enzevalos_iphone/SwiftUI/Read/ReadMainView.swift b/enzevalos_iphone/SwiftUI/Read/ReadMainView.swift index 14516db7e727f5254d8bd73ea572ae6b95178d83..27d7620e3c02924b707cad3652c77f3d7b7e5df3 100644 --- a/enzevalos_iphone/SwiftUI/Read/ReadMainView.swift +++ b/enzevalos_iphone/SwiftUI/Read/ReadMainView.swift @@ -21,7 +21,7 @@ struct ReadMainView <M: DisplayMail>: View { @ObservedObject var model: ReadModel<M> var body: some View { - TabView(selection: $model.currentTab){ + TabView(selection: $model.currentTab) { ForEach(Tabs, id: \.id ){ tab in tab.content .tabItem { diff --git a/enzevalos_iphone/SwiftUI/Read/Tabbed Views/MessageViewMain.swift b/enzevalos_iphone/SwiftUI/Read/Tabbed Views/MessageViewMain.swift index 279655cb7e8a5bda991fff04dcc884e6d1b67a70..ba80bae93e6bcbd138e6cc8ea85e89ba89b0d2f0 100644 --- a/enzevalos_iphone/SwiftUI/Read/Tabbed Views/MessageViewMain.swift +++ b/enzevalos_iphone/SwiftUI/Read/Tabbed Views/MessageViewMain.swift @@ -8,7 +8,6 @@ import SwiftUI - struct MessageViewMain <M: DisplayMail>: View { @EnvironmentObject var model: ReadModel<M> @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode> @@ -16,16 +15,16 @@ struct MessageViewMain <M: DisplayMail>: View { let innerPadding: CGFloat = 20 // button radius let outerPadding: CGFloat = 5 let extraButtonFactor: CGFloat = 0.8 - + @State var showExtraButtons: Bool = false @State var newMail: PreMailData? var body: some View { ZStack{ MessageBody.padding(.horizontal) - .onTapGesture { - self.showExtraButtons = false - } + .onTapGesture { + self.showExtraButtons = false + } FloatingReplyButtons }.onAppear(perform: {self.model.mail.markAsRead(isRead: true)}) .sheet(item: $newMail, content: {mail in ComposeView(preData: mail)}) @@ -47,7 +46,7 @@ struct MessageViewMain <M: DisplayMail>: View { } } } - + var MessageBody : some View { GeometryReader {geometry in @@ -61,11 +60,11 @@ struct MessageViewMain <M: DisplayMail>: View { } var FloatingReplyButtons: some View{ - return VStack{ - Spacer() - HStack{ - Spacer() - ZStack{ + return VStack{ + Spacer() + HStack{ + Spacer() + ZStack{ if model.mail.folderType == FolderType.Archive { forwardButton.offset(y: showExtraButtons ? -innerPadding*9 : 0) replyAllButton.offset(y: showExtraButtons ? -innerPadding*4.5 : 0) @@ -95,28 +94,28 @@ struct MessageViewMain <M: DisplayMail>: View { deleteButton.offset(x: showExtraButtons ? -innerPadding*9 : 0) archiveButton.offset(x: showExtraButtons ? -innerPadding*13.5 : 0) } - //options - FloatingActionButton( - //radius: innerPadding, - onShortPress: {self.showExtraButtons.toggle()} - /*TODO: fill in stuff*/ - ){ - VStack{ - Image(systemName: "arrowshape.turn.up.left.fill") - Text("options").font(.system(size: 7)).fontWeight(.none) - } - }.opacity(showExtraButtons ? 0 : 1) - }.animation(.easeInOut(duration: 0.2)) - }.padding(outerPadding) - }.padding(outerPadding) - } + //options + FloatingActionButton( + //radius: innerPadding, + onShortPress: {self.showExtraButtons.toggle()} + /*TODO: fill in stuff*/ + ){ + VStack{ + Image(systemName: "arrowshape.turn.up.left.fill") + Text("options").font(.system(size: 7)).fontWeight(.none) + } + }.opacity(showExtraButtons ? 0 : 1) + }.animation(.easeInOut(duration: 0.2)) + }.padding(outerPadding) + }.padding(outerPadding) + } var forwardButton: some View { FloatingActionButton( radius: innerPadding*extraButtonFactor, onShortPress: { newMail = model.newUserAction(action: .Forward) - } + } ){ VStack{ Image(systemName: "arrowshape.turn.up.right.fill") @@ -131,7 +130,7 @@ struct MessageViewMain <M: DisplayMail>: View { radius: innerPadding*extraButtonFactor, onShortPress: { newMail = model.newUserAction(action: .ReplyAll) - } + } ){ VStack{ Image(systemName: "arrowshape.turn.up.left.2.fill") @@ -142,10 +141,10 @@ struct MessageViewMain <M: DisplayMail>: View { var unreadButton: some View{ FloatingActionButton( - radius: innerPadding*extraButtonFactor, - onShortPress: { - newMail = model.newUserAction(action: .Unread) - } + radius: innerPadding*extraButtonFactor, + onShortPress: { + newMail = model.newUserAction(action: .Unread) + } ){ VStack{ Image(systemName: "envelope.badge") @@ -156,24 +155,23 @@ struct MessageViewMain <M: DisplayMail>: View { var deleteButton: some View{ FloatingActionButton( - radius: innerPadding*extraButtonFactor, - onShortPress: { - newMail = model.newUserAction(action: .Delete) - }){ + radius: innerPadding*extraButtonFactor, + onShortPress: { + newMail = model.newUserAction(action: .Delete) + }){ VStack{ Image(systemName: "trash") Text("delete").font(.system(size: 7)).fontWeight(.none) } } - } var archiveButton: some View { FloatingActionButton( - radius: innerPadding*extraButtonFactor, - onShortPress: { - newMail = model.newUserAction(action: .Archive) - }){ + radius: innerPadding*extraButtonFactor, + onShortPress: { + newMail = model.newUserAction(action: .Archive) + }){ VStack{ Image(systemName: "folder") Text("archive").font(.system(size: 7)).fontWeight(.none) @@ -185,7 +183,7 @@ struct MessageViewMain <M: DisplayMail>: View { FloatingActionButton( onShortPress: { newMail = model.newUserAction(action: .Reply) - } + } ){ VStack{ Image(systemName: "arrowshape.turn.up.left.fill") diff --git a/enzevalos_iphone/SwiftUI/Read/Tabbed Views/SecurityBriefingView.swift b/enzevalos_iphone/SwiftUI/Read/Tabbed Views/SecurityBriefingView.swift index 335fde2c26014ff2688efe719d4c6a8bc0a2db77..1ea78852bf1656a5d6d86d28818389572ffd8f32 100644 --- a/enzevalos_iphone/SwiftUI/Read/Tabbed Views/SecurityBriefingView.swift +++ b/enzevalos_iphone/SwiftUI/Read/Tabbed Views/SecurityBriefingView.swift @@ -3,6 +3,7 @@ // enzevalos_iphone // // Created by Oliver Wiese on 09.04.20. +// Modified by Chris Offner & Claire Bräuer in March 2021. // Copyright © 2020 fu-berlin. All rights reserved. // diff --git a/enzevalos_iphone/SwiftUI/SupportingViews/MailRow.swift b/enzevalos_iphone/SwiftUI/SupportingViews/MailRow.swift deleted file mode 100644 index a5169df7ddab18f55fc9fb4950bdb1ecf963f465..0000000000000000000000000000000000000000 --- a/enzevalos_iphone/SwiftUI/SupportingViews/MailRow.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// MailRow.swift -// enzevalos_iphone -// -// Created by Oliver Wiese on 27.10.20. -// Copyright © 2020 fu-berlin. All rights reserved. -// - -import SwiftUI - -struct MailRow <M: DisplayMail>: View { - let mail: M - @State var isLinkActive = false - - var body: some View { - HStack(alignment: .center) { - icon - VStack { - HStack{ - Text(mail.subjectWithFlag) - .font(.caption) - .lineLimit(2) - Spacer() - Text(mail.date.timeAgoText()) - .font(.caption) - } - nameView - } - } - } - - private var icon: some View{ - mail.sender.avatar - .resizable() - .frame(width: 60, height: 60) - .shadow(radius: 5) - .onTapGesture(count: /*@START_MENU_TOKEN@*/1/*@END_MENU_TOKEN@*/, perform: { - isLinkActive = true - }) - .background( - NavigationLink(destination: ContactView(contact: mail.sender, fromMail: mail, derivedFromKey: true), isActive: $isLinkActive) { - EmptyView() - } - .hidden() - ) - } - - private var nameView: some View { - Text(mail.sender.name) - .frame(maxWidth: 300, alignment: .leading) - .lineLimit(1) - .font(.subheadline) - } -} - -struct MailRow_Previews: PreviewProvider { - static var previews: some View { - MailRow(mail: ProxyData.PlainMail) - } -} diff --git a/enzevalos_iphone/SwiftUI/SupportingViews/MailRowView.swift b/enzevalos_iphone/SwiftUI/SupportingViews/MailRowView.swift new file mode 100644 index 0000000000000000000000000000000000000000..fa89a176c49db716dfca1b39e1834ee98474eb3f --- /dev/null +++ b/enzevalos_iphone/SwiftUI/SupportingViews/MailRowView.swift @@ -0,0 +1,69 @@ +// +// MailRow.swift +// enzevalos_iphone +// +// Created by Oliver Wiese on 27.10.20. +// Modified by Chris Offner & Claire Bräuer in March 2021. +// Copyright © 2020 fu-berlin. All rights reserved. +// + +import SwiftUI + +struct MailRowView <M: DisplayMail>: View { + let mail: M + + var body: some View { + HStack { + avatar + + VStack(alignment: .leading) { + HStack { + // Sender name + Text(mail.sender.name) + .fontWeight(mail.isRead ? .regular : .medium) + .lineLimit(1) + + Spacer() + + // Arrival time + Text(mail.date.timeAgoText()) + .font(.caption) + } + + HStack { + // Subject + Text(mail.subject) + .font(.caption) + .lineLimit(2) + + Spacer() + + // Attachment indicator if relevant + if !mail.displayAttachments.isEmpty { + Image(systemName: "paperclip") + .font(.caption) + .foregroundColor(.secondary) + } + } + } + .foregroundColor(mail.isRead ? .secondary : .primary) + } + .padding(4) + .frame(height: 65) + } + + private var avatar: some View { + mail.sender.avatar + .resizable() + .aspectRatio(contentMode: .fit) + .shadow(radius: 2) + .opacity(mail.isRead ? 0.45 : 1) + } +} + +struct MailRow_Previews: PreviewProvider { + static var previews: some View { + MailRowView(mail: ProxyData.PlainMail) + .previewLayout(.sizeThatFits) + } +} diff --git a/enzevalos_iphone/SwiftUI/SupportingViews/SearchView.swift b/enzevalos_iphone/SwiftUI/SupportingViews/SearchView.swift index cf10c7c7a73c67d376c99c4f7502bdb1b0357730..57dc3e76c985e7e0e4c9495ec076ee2afc086fb6 100644 --- a/enzevalos_iphone/SwiftUI/SupportingViews/SearchView.swift +++ b/enzevalos_iphone/SwiftUI/SupportingViews/SearchView.swift @@ -9,96 +9,62 @@ import SwiftUI import Combine -/** - Where are we looking for? Used in the search footer. - */ -enum SearchType: CaseIterable { - case Sender, Subject, Body, All - var text: String { - get{ - switch self { - case .Sender: - return NSLocalizedString("Sender", comment: "") - case .Subject: - return NSLocalizedString("Subject", comment: "") - case .Body: - return NSLocalizedString("Body", comment: "") - case .All: - return NSLocalizedString("All", comment: "") - } - } - } - - static func findType(i: Int) -> SearchType{ - if i < SearchType.allCases.count { - return SearchType.allCases[i] - } else { - return SearchType.All - } - } -} /** A SearchView for mails with a search segmented Picker to choose the search area. Open Problems: Deplay the search, s.t. we do not start a search after each input. - */ struct SearchView: View { - @Binding var searchText: String - @Binding var searchField: Int - @Binding var searchNow: Bool - - @State private var showCancelButton: Bool = false + @Binding var searchType: SearchType + @State private var editingSearchField: Bool = false var body: some View { - VStack{ + VStack { HStack { searchFieldView - if showCancelButton { + if editingSearchField { Button("Cancel") { - UIApplication.shared.endEditing(true) - self.searchText = "" - self.showCancelButton = false - self.searchNow = false + UIApplication.shared.endEditing(true) + searchText = "" } - .foregroundColor(Color(.systemBlue)) + .foregroundColor(.accentColor) } } - if showCancelButton { - searchSegment + + if editingSearchField { + searchTypePicker } } - .padding(.horizontal) } var searchFieldView: some View { HStack { Image(systemName: "magnifyingglass") - TextField(NSLocalizedString("Searchbar.Title", comment: "Search"), text: $searchText, onEditingChanged: { isEditing in - self.showCancelButton = true - }, onCommit: { - self.searchNow = true - }) - .foregroundColor(.primary) - Button(action: { - self.searchText = "" - }) { - Image(systemName: "xmark.circle.fill").opacity(searchText == "" ? 0 : 1) + + TextField("Searchbar.Title", text: $searchText) { isEditing in + withAnimation { + editingSearchField = isEditing + } + } + .foregroundColor(.primary) + + Button { + searchText = "" + } label: { + Image(systemName: "xmark.circle.fill") + .opacity(searchText == "" ? 0 : 1) } } - .padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6)) + .padding(8) .foregroundColor(.secondary) .background(Color(.secondarySystemBackground)) .cornerRadius(10.0) } - var searchSegment: some View { - Picker(selection: $searchField, label: EmptyView()) { - ForEach(0..<SearchType.allCases.count) { index in - Text(SearchType.allCases[index].text).tag(index) - .onTapGesture { - self.searchField = index - } + var searchTypePicker: some View { + Picker("Search Filter", selection: $searchType) { + ForEach(SearchType.allCases, id: \.self) { searchType in + Text(searchType.rawValue) } } .pickerStyle(SegmentedPickerStyle()) @@ -112,7 +78,7 @@ struct SearchView: View { extension UIApplication { func endEditing(_ force: Bool) { self.windows - .filter{$0.isKeyWindow} + .filter { $0.isKeyWindow } .first? .endEditing(force) } @@ -132,3 +98,13 @@ extension View { return modifier(ResignKeyboardOnDragGesture()) } } + +/** + Where are we looking for? Used in the search footer. + */ +enum SearchType: LocalizedStringKey, CaseIterable { + case All = "All" + case Sender = "Sender" + case Subject = "Subject" + case Body = "Body" +} diff --git a/enzevalos_iphone/de.lproj/Localizable.strings b/enzevalos_iphone/de.lproj/Localizable.strings index 9645bd31ec6059083cfd1c261c2c42d24bfe969f..d2d4ee71864dd49b301d5678d875a4388b3c2b86 100644 --- a/enzevalos_iphone/de.lproj/Localizable.strings +++ b/enzevalos_iphone/de.lproj/Localizable.strings @@ -6,16 +6,20 @@ Copyright © 2016 fu-berlin. All rights reserved. */ "Permission.contactDenied.Title" = "Oops! Zugriff erlauben!"; -"Permission.contactDenied.Description" = "Bitte erlaube zugriff auf Kontakte In Einstellungen -> Datenschutz -> Kontakte"; +"Permission.contactDenied.Description" = "Bitte erlaube Zugriff auf Kontakte in Einstellungen -> Datenschutz -> Kontakte"; "Permission.Notification.Title" = "Mitteilungen erlauben"; -"Permission.Notification.Description" = "Letterbox möchte dich benachrichtigen. -Du kannst das jederzeit ändern. In der Einstellungen -> Mitteilungen -> Letterbox"; +"Permission.Notification.Description" = "Letterbox möchte dich benachrichtigen. Du kannst das jederzeit ändern in Einstellungen -> Mitteilungen -> Letterbox"; "Permission.AccessContacts.Title" = "Zugriff auf Kontakte"; "Permission.AccessContacts.Description" = "Wir teilen diese Daten mit niemandem und senden sie auch nicht über das Internet."; "AccessNotGranted" = "Bitte schalte unter Einstellungen den Zugriff auf die Kontakte frei, wenn du möchtest, dass die App richtig funktioniert"; "Archive" = "Archiv"; "Address" = "Adresse"; "Addressbook" = "Adressbuch"; +"AddressRecord.lastHeardFrom.recently" = "kürzlich"; +"AddressRecord.lastHeardFrom.lastWeek" = "letzte Woche"; +"AddressRecord.lastHeardFrom.lastMonth" = "letzten Monat"; +"AddressRecord.lastHeardFrom.LongTimeAgo" = "vor langer Zeit"; +"AddressRecord.lastHeardFrom.Never" = "noch nie"; "Attach" = "Anhängen"; "Attachment" = "Anhang"; "Authentification" = "Authentifizierung"; @@ -23,6 +27,10 @@ Du kannst das jederzeit ändern. In der Einstellungen -> Mitteilungen -> Letterb "Bcc" = "Bcc"; "Cancel" = "Abbrechen"; "Cc" = "Cc"; +"CcBcc" = "Cc/Bcc"; +"Name" = "Name"; +"SortBy" = "Sortiere nach"; +"LastContacted" = "zuletzt kontaktiert"; "Compose.Suggestions" = "Empfehlungen"; "Checkmarks" = "Du hast Nachrichten von den Adressen mit Haken bekommen"; "Close" = "Schließen"; diff --git a/enzevalos_iphone/en.lproj/Localizable.strings b/enzevalos_iphone/en.lproj/Localizable.strings index bad3e4d9db6f943d5ccbde29fbff0fba8e90e209..c27a527def77c96f864fa41f3d550ad25ec34b75 100644 --- a/enzevalos_iphone/en.lproj/Localizable.strings +++ b/enzevalos_iphone/en.lproj/Localizable.strings @@ -11,10 +11,15 @@ "Permission.Notification.Description" = "Letterbox wants to notify you. You can change it anytime in Settings -> Notifications -> Letterbox"; "Permission.AccessContacts.Title" = "Access Contacts"; "Permission.AccessContacts.Description" = "Letterbox needs to access your contacts to work properly. Your data would be safe and not accessible by third parties or shared on the internet."; -"AccessNotGranted" = "Please allow access to contacts in settings, if you want the app to work properly."; // TODO +"AccessNotGranted" = "Please allow access to contacts in settings, if you want the app to work properly."; "Archive" = "Archive"; "Address" = "Address"; "Addressbook" = "Contracts"; +"AddressRecord.lastHeardFrom.recently" = "recently"; +"AddressRecord.lastHeardFrom.lastWeek" = "last week"; +"AddressRecord.lastHeardFrom.lastMonth" = "last month"; +"AddressRecord.lastHeardFrom.LongTimeAgo" = "a long time ago"; +"AddressRecord.lastHeardFrom.Never" = "never"; "Attach" = "Attach"; "Attachment" = "Attachment"; "Authentification" = "Authentification"; @@ -22,6 +27,10 @@ "Bcc" = "Bcc"; "Cancel" = "Cancel"; "Cc" = "Cc"; +"CcBcc" = "Cc/Bcc"; +"SortBy" = "Sort by"; +"Name" = "Name"; +"LastContacted" = "Last Contacted"; "Compose.Suggestions" = "Suggestions"; "Checkmarks" = "You received mails from addresses with checkmarks"; // ???? "Close" = "Close"; diff --git a/enzevalos_iphone/persistentData/AddressRecord.swift b/enzevalos_iphone/persistentData/AddressRecord.swift index a43ac022d2f79f3e5de4a29727ce1d547750ebd2..416e9a54b7eb34e7d446344cdd827bdadabba75f 100644 --- a/enzevalos_iphone/persistentData/AddressRecord.swift +++ b/enzevalos_iphone/persistentData/AddressRecord.swift @@ -16,7 +16,7 @@ extension AddressRecord { /// Enum defining which key to to sort contacts by (name, recency). enum SortBy: String { case name = "displayname" - case recency = "last" // still unavailable + case recency = "email" // TODO: change to "last" once that data is implemented } private static var sorting: [NSSortDescriptor] { @@ -82,9 +82,8 @@ extension AddressRecord { public var name: String { get { - if let contact = self.phoneBookID { - // TODO Look up cn contact name! - } + // TODO: Look up CN contact name! +// if let contact = self.phoneBookID {} if let displayname = self.displayname { return displayname } @@ -190,21 +189,21 @@ extension AddressRecord: DisplayContact { } /// Casual string for recency of correspondence. - var lastHeardFrom: String { + var lastHeardFrom: LocalizedStringKey { if let lastHeard = last { let now = Date() switch now.timeIntervalSince(lastHeard) { case 0..<259200: - return "recently" + return "AddressRecord.lastHeardFrom.recently" case 259200..<604800: - return "last week" + return "AddressRecord.lastHeardFrom.lastWeek" case 259200..<2592000: - return "last month" + return "AddressRecord.lastHeardFrom.lastMonth" default: - return "a long time ago" + return "AddressRecord.lastHeardFrom.LongTimeAgo" } } else { - return "never" + return "AddressRecord.lastHeardFrom.Never" } } } diff --git a/enzevalos_iphone/persistentData/FolderRecord.swift b/enzevalos_iphone/persistentData/FolderRecord.swift index 668199e9ecd2a5e12b97087a86656f4c0f5eeb2c..06db902ecc33a4271253d0f55324c174bfd226fd 100644 --- a/enzevalos_iphone/persistentData/FolderRecord.swift +++ b/enzevalos_iphone/persistentData/FolderRecord.swift @@ -43,8 +43,11 @@ extension FolderRecord { var uids: MCOIndexSet { get { let indexSet = MCOIndexSet() - if let set = self.mailsInFolder, set.count > 0, let mails = set.allObjects as? [MailRecord] { - mails.forEach({indexSet.add(UInt64($0.uID))}) + if let set = self.mailsInFolder, set.count > 0, + let mails = set.allObjects as? [MailRecord] { + mails.forEach { + indexSet.add(UInt64($0.uID)) + } } return indexSet } @@ -71,14 +74,17 @@ extension FolderRecord { } static func lastDate(folder: String, date: Date) { - try? PersistentDataProvider.dataProvider.updateFolder(folder: folder, performingUpdates: {$0.lastUpdate = date}) - + try? PersistentDataProvider.dataProvider.updateFolder(folder: folder) { + $0.lastUpdate = date + } } } extension FolderRecord: DisplayFolder { var name: String { - if let delimiter = delimiter?.first, let n = path?.split(separator: delimiter), let last = n.last { + if let delimiter = delimiter?.first, + let n = path?.split(separator: delimiter), + let last = n.last { return String(last) } return path ?? "NO NAME" @@ -116,6 +122,4 @@ extension FolderRecord: DisplayFolder { } return 0 } - - } diff --git a/enzevalos_iphone/persistentData/MailRecord+CoreDataProperties.swift b/enzevalos_iphone/persistentData/MailRecord+CoreDataProperties.swift index e43ec6064abddb07dacc902c1bfa1127c6adad8d..0b12c7c537331b3316a362a2822f4b73f9e4a36e 100644 --- a/enzevalos_iphone/persistentData/MailRecord+CoreDataProperties.swift +++ b/enzevalos_iphone/persistentData/MailRecord+CoreDataProperties.swift @@ -36,7 +36,6 @@ extension MailRecord { @NSManaged public var inFolder: FolderRecord? @NSManaged public var signatureKey: PublicKeyRecord? @NSManaged public var toAddresses: NSSet? - } // MARK: Generated accessors for attachedPublicKeys diff --git a/enzevalos_iphone/persistentData/MailRecord.swift b/enzevalos_iphone/persistentData/MailRecord.swift index c107320e5c63db63b663b8881ab7bfd312648fcc..57f9e0b7c15e7695fb18acc7dd6023c3701f1f7b 100644 --- a/enzevalos_iphone/persistentData/MailRecord.swift +++ b/enzevalos_iphone/persistentData/MailRecord.swift @@ -73,7 +73,7 @@ extension MailRecord { return [] } - var messageFlag: MCOMessageFlag { + var messageFlags: MCOMessageFlag { get { return MCOMessageFlag.init(rawValue: Int(flag)) } @@ -94,12 +94,10 @@ extension MailRecord { } let messageArray = body.components(separatedBy: "\n") return messageArray.joined(separator: " ") - } } extension MailRecord: DisplayMail { - var encryptionType: CryptoScheme? { // TODO: Consider unable to decrypt case. Extend mail record? if let key = decryptionKey { @@ -120,30 +118,29 @@ extension MailRecord: DisplayMail { if !self.isRead { returnString.append("🔵 ") } - if MCOMessageFlag.answered.isSubset(of: messageFlag) { + if MCOMessageFlag.answered.isSubset(of: messageFlags) { returnString.append("↩️ ") } - if MCOMessageFlag.forwarded.isSubset(of: messageFlag) { + if MCOMessageFlag.forwarded.isSubset(of: messageFlags) { returnString.append("➡️ ") } - if MCOMessageFlag.flagged.isSubset(of: messageFlag) { + if MCOMessageFlag.flagged.isSubset(of: messageFlags) { returnString.append("⭐️ ") } return "\(returnString)\(subject)" } - var isRead: Bool { get { let mcoflag = MCOMessageFlag.init(rawValue: Int(flag)) let value = mcoflag.contains(MCOMessageFlag.seen) return value } + set { var mcoflag = MCOMessageFlag.init(rawValue: Int(flag)) mcoflag = mcoflag.update(with: .seen) ?? mcoflag self.flag = Int16(mcoflag.rawValue) - } } @@ -205,14 +202,15 @@ extension MailRecord: DisplayMail { } func markAsRead(isRead: Bool) { + + // TODO: FIX! Faults the current objects... +// if let context = self.managedObjectContext { +// var newFlag = self.messageFlag +// newFlag.insert(.seen) +// flag = Int16(newFlag.rawValue) +// try? PersistentDataProvider.dataProvider.save(taskContext: context) // <- later? +// } return - // TODO FIX! Faults the current objects... - if let context = self.managedObjectContext { - var newFlag = self.messageFlag - newFlag.insert(.seen) - flag = Int16(newFlag.rawValue) - try? PersistentDataProvider.dataProvider.save(taskContext: context) // <- later? - } } var transportEnc: Bool { @@ -222,8 +220,7 @@ extension MailRecord: DisplayMail { } } -extension MailRecord: Identifiable { -} +extension MailRecord: Identifiable {} import SwiftUI extension MailRecord {