diff --git a/enzevalos_iphone.xcodeproj/project.pbxproj b/enzevalos_iphone.xcodeproj/project.pbxproj index 43aac933efafd2f567e1dcdba1d5ce64ab07c0b3..1ee1f83d6e1a59d546c2e62f13820f1735dadbf6 100644 --- a/enzevalos_iphone.xcodeproj/project.pbxproj +++ b/enzevalos_iphone.xcodeproj/project.pbxproj @@ -165,6 +165,10 @@ 8428A8711F436A1E007649A5 /* GamificationStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428A86D1F436A1E007649A5 /* GamificationStatusViewController.swift */; }; 8428A8831F436AC9007649A5 /* GamificationDataUnitTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428A8561F4369EA007649A5 /* GamificationDataUnitTest.swift */; }; 8428A8841F436ACC007649A5 /* GamificationElements.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8428A8541F4369CF007649A5 /* GamificationElements.xcassets */; }; + 976EEBED240D47C3006FE574 /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976EEBEC240D47C3006FE574 /* AuthenticationViewModel.swift */; }; + 9771AA8C241161190023A096 /* MailAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9771AA8B241161190023A096 /* MailAccount.swift */; }; + 97AACD2424178C230078A68E /* AuthenticationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97AACD2324178C230078A68E /* AuthenticationModel.swift */; }; + 97B2F4D6240D321B000DB34E /* AuthenticationScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B2F4D5240D321B000DB34E /* AuthenticationScreen.swift */; }; 988C9C5D240D507A006213F0 /* PhishingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 988C9C5C240D507A006213F0 /* PhishingTests.swift */; }; A102AA8A1EDDB4F40024B457 /* videoOnboarding2.m4v in Resources */ = {isa = PBXBuildFile; fileRef = A102AA891EDDB4E80024B457 /* videoOnboarding2.m4v */; }; A1083A541E8BFEA6003666B7 /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1083A531E8BFEA6003666B7 /* Onboarding.swift */; }; @@ -562,6 +566,10 @@ 8B87EFB6CEAA31452F744015 /* Pods-enzevalos_iphoneUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-enzevalos_iphoneUITests.release.xcconfig"; path = "../enzevalos_iphone_workspace/Pods/Target Support Files/Pods-enzevalos_iphoneUITests/Pods-enzevalos_iphoneUITests.release.xcconfig"; sourceTree = "<group>"; }; 91B6C9020C660BEA78FAEF28 /* Pods-enzevalos_iphone.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-enzevalos_iphone.debug.xcconfig"; path = "../enzevalos_iphone_workspace/Pods/Target Support Files/Pods-enzevalos_iphone/Pods-enzevalos_iphone.debug.xcconfig"; sourceTree = "<group>"; }; 94EE54279AB591E0CAB8EFD8 /* Pods_enzevalos_iphone.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_enzevalos_iphone.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 976EEBEC240D47C3006FE574 /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; }; + 9771AA8B241161190023A096 /* MailAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailAccount.swift; sourceTree = "<group>"; }; + 97AACD2324178C230078A68E /* AuthenticationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationModel.swift; sourceTree = "<group>"; }; + 97B2F4D5240D321B000DB34E /* AuthenticationScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationScreen.swift; sourceTree = "<group>"; }; 988C9C5C240D507A006213F0 /* PhishingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhishingTests.swift; sourceTree = "<group>"; }; 9A132EDE8BCA06ACDB505C22 /* Pods-enzevalos_iphoneUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-enzevalos_iphoneUITests.debug.xcconfig"; path = "../enzevalos_iphone_workspace/Pods/Target Support Files/Pods-enzevalos_iphoneUITests/Pods-enzevalos_iphoneUITests.debug.xcconfig"; sourceTree = "<group>"; }; 9B3D62838C729BAC6832270A /* Pods-enzevalos_iphone-AdHoc.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-enzevalos_iphone-AdHoc.debug.xcconfig"; path = "../enzevalos_iphone_workspace/Pods/Target Support Files/Pods-enzevalos_iphone-AdHoc/Pods-enzevalos_iphone-AdHoc.debug.xcconfig"; sourceTree = "<group>"; }; @@ -1186,6 +1194,17 @@ name = Data; sourceTree = "<group>"; }; + 97B2F4D4240D31E6000DB34E /* authentication */ = { + isa = PBXGroup; + children = ( + 97B2F4D5240D321B000DB34E /* AuthenticationScreen.swift */, + 976EEBEC240D47C3006FE574 /* AuthenticationViewModel.swift */, + 9771AA8B241161190023A096 /* MailAccount.swift */, + 97AACD2324178C230078A68E /* AuthenticationModel.swift */, + ); + name = authentication; + sourceTree = "<group>"; + }; A10DE41E1EFAA140005E8189 /* folders */ = { isa = PBXGroup; children = ( @@ -1945,6 +1964,7 @@ 479C649A21F45DAF00A01071 /* HideShowPasswordTextField.swift in Sources */, 47CEAC98222541B40075B7DC /* MailSession.swift in Sources */, F119D2901E364B59001D732A /* AnimatedSendIcon.swift in Sources */, + 97AACD2424178C230078A68E /* AuthenticationModel.swift in Sources */, 4707096D1F8F9F4900657F41 /* ExportViewController.swift in Sources */, F12060801DA540FE00F6EF37 /* RefreshControlExtension.swift in Sources */, 4751C6FC2344C8D1006B2A4D /* KeyTableViewController.swift in Sources */, @@ -2015,6 +2035,7 @@ F18B445E1E7044B70080C041 /* FlipTransition.swift in Sources */, 472F397E1E1D0B0B009260FB /* EnzevalosContact+CoreDataProperties.swift in Sources */, 4751C6FA23449699006B2A4D /* CryptoManagementViewController.swift in Sources */, + 9771AA8C241161190023A096 /* MailAccount.swift in Sources */, 478154AC21FF6A9600A931EC /* Mailbot.swift in Sources */, 8428A86E1F436A1E007649A5 /* BadgeCase.swift in Sources */, A1B49E5D21E54CBF00ED86FC /* IntroContactTableViewController.swift in Sources */, diff --git a/enzevalos_iphone/AppDelegate.swift b/enzevalos_iphone/AppDelegate.swift index 339c577bfec2e1f1bca5e8ad7f7afa7d8adef2e6..4bc3223bf8fd5be45d344ca42fabd24a71a4dc2b 100644 --- a/enzevalos_iphone/AppDelegate.swift +++ b/enzevalos_iphone/AppDelegate.swift @@ -32,7 +32,7 @@ import SwiftUI class AppDelegate: UIResponder, UIApplicationDelegate { - var newOnboarding=false + var newOnboarding = true var window: UIWindow? var contactStore = CNContactStore() @@ -64,7 +64,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { self.window = UIWindow(frame: UIScreen.main.bounds) if (newOnboarding) - {self.window?.rootViewController = UIHostingController(rootView: NewOnboardingView())} + {self.window?.rootViewController = UIHostingController(rootView: AuthenticationScreen())} else {self.window?.rootViewController = Onboarding.onboarding()} self.window?.makeKeyAndVisible() diff --git a/enzevalos_iphone/AuthenticationModel.swift b/enzevalos_iphone/AuthenticationModel.swift new file mode 100644 index 0000000000000000000000000000000000000000..650953c5cda8c6f0ccc3c6b4e8213f1b350dfbd8 --- /dev/null +++ b/enzevalos_iphone/AuthenticationModel.swift @@ -0,0 +1,196 @@ +// +// AuthenticationModel.swift +// enzevalos_iphone +// +// Created by SWP Usable Security für Smartphones on 10.03.20. +// Copyright © 2020 fu-berlin. All rights reserved. +// + +import Foundation +import Combine + +/** + Model of the Authentication according to MVVP pattern. Performs all the necessary server calls according to the data provided by the ViewModel + */ +class AuthenticationModel: NSObject { + + enum AuthenticationResult {case Success, Timeout, Error(value: MailServerConnectionError)} + + static let instance: AuthenticationModel = AuthenticationModel() + + var extendedValidation: Bool = false + + private var currentIMAP: MailSession? + private var currentSMTP: MailSession? + var imapCallback: AuthenticationCallback? = nil + var smtpCallback: AuthenticationCallback? = nil + + var imapConfigurationSuccessful = false + var smtpConfigurationSuccessful = false + + var dispatchGroup = DispatchGroup() + + /** + Start asynchronous tasks for checking IMAP and SMTP configuration with a *timeoutDelay* after which the tasks are not getting consumed anymore. + + - Parameters: + - mailAccount: data class that holds the properties used for establishing the connection + - extendedValidation: indicates whether the imap/smtp configuration got specified by the user or should use default values + + - Returns: a Future that produces an AuthenticationResult once the server calls are done + */ + func checkConfig(mailAccount: MailAccount, extendedValidation: Bool) -> Future<AuthenticationResult, Never> { + self.extendedValidation = extendedValidation + var future: Future<AuthenticationResult, Never> + dispatchGroup = DispatchGroup() + imapCallback = AuthenticationCallback(callback: onImapCompleted) + let imapItem = DispatchWorkItem { self.checkIMAPConfig(mailAccount: mailAccount, self.imapCallback! ) } + dispatchGroup.enter() + DispatchQueue.global().async(execute: imapItem) + + smtpCallback = AuthenticationCallback(callback: onSmtpCompleted) + let smtpItem = DispatchWorkItem { self.checkSMTPConfig(mailAccount: mailAccount, self.smtpCallback!) } + dispatchGroup.enter() + DispatchQueue.global().async(execute: smtpItem) + + future = Future { promise in + DispatchQueue.global().async { + let timeoutDelay: DispatchTimeInterval = DispatchTimeInterval.seconds(10) + let result = self.dispatchGroup.wait(timeout: DispatchTime.now() + timeoutDelay) + DispatchQueue.main.async { + promise(.success(self.onTimeout(timeoutResult: result))) + } + } + } + return future + } + + func checkIMAPConfig(mailAccount: MailAccount, _ callback: AuthenticationCallback) { + imapConfigurationSuccessful = false + let mailSession: MailSession = setupIMAPSession(mailAccount: mailAccount, callback: callback) + currentIMAP = mailSession + if extendedValidation && mailSession.startTestingServerConfig(){ + imapConfigurationSuccessful = true + } else { + if !mailSession.hasJsonFile && mailSession.startLongSearchOfServerConfig(hostFromAdr: false){ + imapConfigurationSuccessful = true + } else { + if mailSession.startTestingServerConfigFromList() || mailSession.startLongSearchOfServerConfig(hostFromAdr: true){ + imapConfigurationSuccessful = true + } + } + } + } + + func checkSMTPConfig(mailAccount: MailAccount, _ callback: AuthenticationCallback) { + smtpConfigurationSuccessful = false + let mailSession: MailSession = setupSMTPSession(mailAccount: mailAccount, callback: callback) + currentSMTP = mailSession + if extendedValidation && mailSession.startTestingServerConfig() { + smtpConfigurationSuccessful = true + } else { + if !mailSession.hasJsonFile && mailSession.startLongSearchOfServerConfig(hostFromAdr: false) { + smtpConfigurationSuccessful = true + } else { + if mailSession.startTestingServerConfigFromList() || mailSession.startLongSearchOfServerConfig(hostFromAdr: true){ + smtpConfigurationSuccessful = true + } + } + } + } + + private func setupIMAPSession(mailAccount: MailAccount, callback: AuthenticationCallback) -> MailSession { + let mailSession = MailSession(configSession: SessionType.IMAP, mailAddress: mailAccount.emailAddress, password: mailAccount.password, username: mailAccount.username) + if extendedValidation { + let imapConnValue = mailAccount.imapEncryption + mailSession.setServer(hostname: mailAccount.imapServer, port: UInt32(mailAccount.imapPort), connType: imapConnValue, authType: mailAccount.authType) + } + let listenerIMAP = Listener(callback: callback, mailAccount: mailAccount) + mailSession.addListener(listener: listenerIMAP) + return mailSession + } + + private func setupSMTPSession(mailAccount: MailAccount, callback: AuthenticationCallback) -> MailSession { + let mailSession = MailSession(configSession: SessionType.SMTP, mailAddress: mailAccount.emailAddress, password: mailAccount.password, username: mailAccount.username) + if extendedValidation { + let smtpConnValue = mailAccount.smtpEncryption + mailSession.setServer(hostname: mailAccount.smtpServer, port: UInt32(mailAccount.smtpPort), connType: smtpConnValue, authType: mailAccount.authType) + } + let listenerSMTP = Listener(callback: callback, mailAccount: mailAccount) + mailSession.addListener(listener: listenerSMTP) + return mailSession + } + + func onTimeout(timeoutResult: DispatchTimeoutResult) -> AuthenticationResult { + imapCallback?.callback = nil + smtpCallback?.callback = nil + if timeoutResult == .success { + if imapConfigurationSuccessful && smtpConfigurationSuccessful { + return AuthenticationResult.Success + } else { + var error = MailServerConnectionError.AuthenticationError + if let smtp = currentIMAP, let e = MailServerConnectionError.findPrioError(errors: smtp.errors) { + error = e + } + if let imap = currentIMAP, let e = MailServerConnectionError.findPrioError(errors: imap.errors) { + error = e + } + + return AuthenticationResult.Error(value: error) + } + } else { + return AuthenticationResult.Timeout + } + } + + func onImapCompleted(imapWorks: Bool, _ login: String, _ password: String) { + if imapWorks { + _ = currentIMAP?.storeToUserDefaults() + imapConfigurationSuccessful = true + } else { + imapConfigurationSuccessful = false + } + dispatchGroup.leave() + } + + func onSmtpCompleted(smtpWorks: Bool, _ login: String, _ password: String) { + if smtpWorks { + _ = currentSMTP?.storeToUserDefaults() + smtpConfigurationSuccessful = true + } else { + smtpConfigurationSuccessful = false + } + dispatchGroup.leave() + } + + /** + A listner class that notifies the AuthenticationCallback's about the result once the server call is done + */ + class Listener: MailSessionListener { + let callback: AuthenticationCallback + let mailAccount: MailAccount + + init(callback: AuthenticationCallback, mailAccount: MailAccount) { + self.callback = callback + self.mailAccount = mailAccount + } + + func testFinish(result: Bool) { + callback.onResult(worked: result, login: mailAccount.emailAddress, password: mailAccount.password) + } + } +} + +class AuthenticationCallback { + var callback: ((Bool, String, String) -> Void)? + + init(callback: @escaping (Bool, String, String) -> Void) { + self.callback = callback + } + + func onResult(worked: Bool, login: String, password: String) { + if let callback = callback { + callback(worked, login, password) + } + } +} diff --git a/enzevalos_iphone/AuthenticationScreen.swift b/enzevalos_iphone/AuthenticationScreen.swift index 3ba5aee4cc42a261e52e4c42085e25ceccb7e613..b4945a7fc601b663cdf933b9978836c3964e37f4 100644 --- a/enzevalos_iphone/AuthenticationScreen.swift +++ b/enzevalos_iphone/AuthenticationScreen.swift @@ -2,12 +2,15 @@ // AuthenticationScreen.swift // enzevalos_iphone // -// Created by Cezary Pilaszewicz on 02.03.20. +// Created by SWP Usable Security für Smartphones on 02.03.20. // Copyright © 2020 fu-berlin. All rights reserved. // import SwiftUI +/** + View of the Authentication according to MVVP pattern. Observes the AuthenticationViewModel and adjusts the displayed data accordingly + */ struct AuthenticationScreen: View { @State private var login: String = "" @State private var password: String = "" @@ -19,75 +22,75 @@ struct AuthenticationScreen: View { @State private var imapEncryption = 0 @State private var smtpEncryption = 0 - @ObservedObject private var viewModel = AuthenticationViewModel() - var encryptionOptions = ["Plaintext", "StartTLS", "TLS/SSL"] + @ObservedObject private var viewModel = AuthenticationViewModel(authenticationModel: AuthenticationModel.instance) var body: some View { - ScrollView{ + ScrollView { ZStack { Color.white.edgesIgnoringSafeArea(.all) - VStack { - Text("Please enter Your credentials").padding().foregroundColor(Color.yellow) - Text("Login") - TextField("Please enter your login", text: $login).textFieldStyle(RoundedBorderTextFieldStyle()) - Text("Password") - SecureField("Please enter your password", text: $password).textFieldStyle(RoundedBorderTextFieldStyle()) - - HStack { - Toggle(isOn: self.$viewModel.isDetailedAuthentication) { - Text("Advanced options") - } - Spacer() - Button(action: {self.viewModel.isDetailedAuthentication ? - self.viewModel.validate(self.login, self.password, self.username, self.imapServer, self.imapPort, self.imapEncryption, self.smtpServer, self.smtpPort, self.smtpEncryption) : - self.viewModel.validate(self.login, self.password) - }) { - Text("Login") - } - } - - if self.viewModel.isDetailedAuthentication { - Text("Username") - TextField("Please enter your username", text: $username).textFieldStyle(RoundedBorderTextFieldStyle()) + LoadingView(isShowing: self.$viewModel.showProgressSpinner) { + VStack { + Text("Please enter Your credentials").padding().foregroundColor(Color.yellow) + Text("Login") + TextField("Please enter your login", text: self.$login).textFieldStyle(RoundedBorderTextFieldStyle()) + Text("Password") + SecureField("Please enter your password", text: self.$password).textFieldStyle(RoundedBorderTextFieldStyle()) HStack { - Text("Imap server") - TextField("e.g. imap.web.de", text: $imapServer) - } - HStack { - Text("Imap port") - TextField("e.g. 993", text:$imapPort).keyboardType(.numberPad) - } - Picker(selection: $imapEncryption, label: Text("IMAP-Transportencryption")) { - ForEach(0..<encryptionOptions.count) { - Text(self.encryptionOptions[$0]) + Toggle(isOn: self.$viewModel.isDetailedAuthentication) { + Text("Advanced options") + } + Spacer() + Button(action: {self.viewModel.isDetailedAuthentication ? + self.viewModel.detailValidation(self.login, self.password, self.username, self.imapServer, self.imapPort, AuthenticationViewModel.encryptionOptions[self.imapEncryption].value, self.smtpServer, self.smtpPort, AuthenticationViewModel.encryptionOptions[self.smtpEncryption].value) : + self.viewModel.validate(self.login, self.password) + }) { + Text("Login") } - } - HStack { - Text("Smtp server") - TextField("e.g. smtp.web.de", text: $smtpServer) - } - HStack { - Text("Smtp port") - TextField("e.g. 587", text: $smtpPort).keyboardType(.numberPad) } - Picker(selection: $smtpEncryption, label: Text("SMTP-Transportencryption")) { - ForEach(0..<encryptionOptions.count) { - Text(self.encryptionOptions[$0]) + if self.viewModel.isDetailedAuthentication { + Text("Username") + TextField("Please enter your nickname", text: self.$username).textFieldStyle(RoundedBorderTextFieldStyle()) + + HStack { + Text("Imap server") + TextField("e.g. imap.web.de", text: self.$imapServer) + } + HStack { + Text("Imap port") + TextField("e.g. 993", text:self.$imapPort).keyboardType(.numberPad) + } + Picker(selection: self.$imapEncryption, label: Text("IMAP-Transportencryption")) { + ForEach(0..<AuthenticationViewModel.encryptionOptions.count) { + Text(AuthenticationViewModel.encryptionOptions[$0].name) + } + } + HStack { + Text("Smtp server") + TextField("e.g. smtp.web.de", text: self.$smtpServer) + } + HStack { + Text("Smtp port") + TextField("e.g. 587", text: self.$smtpPort).keyboardType(.numberPad) + } + + Picker(selection: self.$smtpEncryption, label: Text("SMTP-Transportencryption")) { + ForEach(0..<AuthenticationViewModel.encryptionOptions.count) { + Text(AuthenticationViewModel.encryptionOptions[$0].name) + } } } - } - - Button(action: { self.viewModel.oauth() }) { - Text("Google login") - } - - }.padding() + + Button(action: { self.viewModel.startGoogleOauth() }) { + Text("Google login") + } + + }.padding() + } - //TODO: once SWIFTUI supports optionals improve this if statement - if self.viewModel.errorMessage != nil && !self.viewModel.errorMessage!.isEmpty { + if shouldDisplayErrorMessage() { VStack { Text(self.viewModel.errorMessage!) .foregroundColor(Color.white) @@ -98,5 +101,48 @@ struct AuthenticationScreen: View { } } } + + func shouldDisplayErrorMessage() -> Bool { + return (self.viewModel.errorMessage != nil) && (!self.viewModel.errorMessage!.isEmpty) + } +} + +struct ActivityIndicator: UIViewRepresentable { + + @Binding var isAnimating: Bool + let style: UIActivityIndicatorView.Style + + func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView { + return UIActivityIndicatorView(style: style) + } + + func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) { + isAnimating ? uiView.startAnimating() : uiView.stopAnimating() + } } +struct LoadingView<Content>: View where Content: View { + + @Binding var isShowing: Bool + var content: () -> Content + + var body: some View { + ZStack(alignment: .center) { + + self.content() + .disabled(self.isShowing) + .blur(radius: self.isShowing ? 3 : 0) + + VStack { + Text("Loading...") + ActivityIndicator(isAnimating: .constant(true), style: .large) + } + .frame(width: UIScreen.main.bounds.size.width / 2, + height: UIScreen.main.bounds.size.height / 5) + .background(Color.secondary.colorInvert()) + .foregroundColor(Color.primary) + .cornerRadius(20) + .opacity(self.isShowing ? 1 : 0) + } + } +} diff --git a/enzevalos_iphone/AuthenticationViewModel.swift b/enzevalos_iphone/AuthenticationViewModel.swift index 6b06149e52006a31d5a4b8c0ea5249e4345c0c97..cfe61434940d087aae915ac2ec90aa3e252d1d79 100644 --- a/enzevalos_iphone/AuthenticationViewModel.swift +++ b/enzevalos_iphone/AuthenticationViewModel.swift @@ -2,111 +2,66 @@ // AuthenticationViewModel.swift // enzevalos_iphone // -// Created by Cezary Pilaszewicz on 02.03.20. +// Created by SWP Usable Security für Smartphones on 02.03.20. // Copyright © 2020 fu-berlin. All rights reserved. // import Foundation +import Combine +/** + ViewModel of the Authentication according to MVVP pattern. Uses AuthenticationModel to get the result of the authentication. + */ class AuthenticationViewModel : ObservableObject { - - static let DEFAULT_IMAP_PORT = 993 - static let DEFAULT_SMTP_PORT = 587 - + @Published var errorMessage: String? @Published var isDetailedAuthentication: Bool = false + @Published var showProgressSpinner: Bool = false - var login:String = "" + let model: AuthenticationModel + var cancellable: AnyCancellable? - @Published var imapServer: String = "imap.example.com" - @Published var imapPort: String = String(DEFAULT_IMAP_PORT) - @Published var imapTransportEncryption = 2 - - @Published var smtpServer: String = "smtp.example.com" - @Published var smtpPort: String = String(DEFAULT_SMTP_PORT) - @Published var smtpTransportEncryption = 1 + static let encryptionOptions: [(name: String, value: Int)] = [(name: "Plaintext", value: MCOConnectionType.clear.rawValue), (name: "StartTLS", value: MCOConnectionType.startTLS.rawValue), (name: "TLS/SSL", value: MCOConnectionType.TLS.rawValue)] + + init(authenticationModel: AuthenticationModel) { + self.model = authenticationModel + } - var isGoogleAuth = false - - var imapConfigurationSuccessful = false - var smtpConfigurationSuccessful = false - var startTimeIMAPCheck: Date? - - var startTimeView = Date() - var transportRows: [Int: String] = [MCOConnectionType.clear.rawValue: NSLocalizedString("Plaintext", comment: ""), MCOConnectionType.startTLS.rawValue: "StartTLS", MCOConnectionType.TLS.rawValue: "TLS/SSL"] - - private var previousIMAP: MailSession? - private var previousSMTP: MailSession? - private var currentIMAP: MailSession? - private var currentSMTP: MailSession? - func validate(_ login: String, _ password: String) -> Void { - checkIMAPConfig(login, password) - } - - func validate(_ login: String, _ password: String, _ username: String, _ imapServer: String, _ imapPort: String, _ imapEncryption: Int, _ smtpServer: String, _ smtpPort: String, _ smtpEncryption: Int) -> Void { - self.login = login - self.imapServer = imapServer - self.imapPort = imapPort - self.imapTransportEncryption = imapEncryption - self.smtpServer = smtpServer - self.smtpPort = smtpPort - self.smtpTransportEncryption = smtpEncryption - checkDetailConfig(imap: true, login, password, username: username, imapServer, Int(imapPort)!, imapEncryption, smtpServer, Int(smtpPort)!, smtpEncryption) - } - - func checkIMAPConfig(_ login: String, _ password: String) { - var mailSession: MailSession - if isGoogleAuth, let session = currentIMAP, session.startTestingServerConfig() { - mailSession = session - imapConfigurationSuccessful = true - startTimeIMAPCheck = Date() - } else { - mailSession = setupIMAPSession(login, password) - if isDetailedAuthentication { - checkDetailConfig(imap: true, login, password, imapServer, Int(imapPort)!, imapTransportEncryption, smtpServer, Int(smtpPort)!, smtpTransportEncryption) - return - } else if !mailSession.hasJsonFile && mailSession.startLongSearchOfServerConfig(hostFromAdr: true) { - imapConfigurationSuccessful = true - startTimeIMAPCheck = Date() - } else { - if mailSession.startTestingServerConfigFromList() || mailSession.startLongSearchOfServerConfig(hostFromAdr: true){ - imapConfigurationSuccessful = true - startTimeIMAPCheck = Date() - } + let mailAccount = MailAccount(emailAddress: login, password: password) + + showProgressSpinner = true + cancellable = model.checkConfig(mailAccount: mailAccount, extendedValidation: false).sink { promise in + switch promise { + case AuthenticationModel.AuthenticationResult.Success : + self.authenticationSucceed() + case AuthenticationModel.AuthenticationResult.Error(let value) : + self.authenticationFailed(error: value) + case AuthenticationModel.AuthenticationResult.Timeout : + self.timeoutNotification() } + self.showProgressSpinner = false } - currentIMAP = mailSession } - func checkSMTPConfig(_ login: String, _ password: String) { - var mailSession: MailSession - if isGoogleAuth, let session = currentSMTP, session.startTestingServerConfig() { - mailSession = session - smtpConfigurationSuccessful = true - } else { - startTimeIMAPCheck = nil - mailSession = setupSMTPSession(login, password) - if isDetailedAuthentication { - checkDetailConfig(imap: false, login, password, imapServer, Int(imapPort)!, imapTransportEncryption, smtpServer, Int(smtpPort)!, smtpTransportEncryption) - return - } else if !mailSession.hasJsonFile && mailSession.startLongSearchOfServerConfig(hostFromAdr: !isDetailedAuthentication) { - smtpConfigurationSuccessful = true - } else { - if mailSession.startTestingServerConfigFromList() || mailSession.startLongSearchOfServerConfig(hostFromAdr: true){ - smtpConfigurationSuccessful = true - } + func detailValidation(_ login: String, _ password: String, _ username: String, _ imapServer: String, _ imapPort: String, _ imapEncryption: Int, _ smtpServer: String, _ smtpPort: String, _ smtpEncryption: Int) -> Void { + let mailAccount = MailAccount(emailAddress: login, password: password, username: username, imapServer: imapServer, imapPort: Int(imapPort)!, imapEncryption: imapEncryption, smtpServer: smtpServer, smtpPort: Int(smtpPort)!, smtpEncryption: smtpEncryption) + + showProgressSpinner = true + cancellable = model.checkConfig(mailAccount: mailAccount, extendedValidation: true).sink { promise in + switch promise { + case AuthenticationModel.AuthenticationResult.Success : + self.authenticationSucceed() + case AuthenticationModel.AuthenticationResult.Error(let value) : + self.authenticationFailed(error: value) + case AuthenticationModel.AuthenticationResult.Timeout : + self.timeoutNotification() } + self.showProgressSpinner = false } - currentSMTP = mailSession - } - - func oauth() { - isGoogleAuth = true - googleLogin() } - func googleLogin() { + func startGoogleOauth() { guard let vc = AppDelegate.getAppDelegate().window?.rootViewController else { print("No view controller!") return @@ -116,127 +71,37 @@ class AuthenticationViewModel : ObservableObject { EmailHelper.singleton().doEmailLoginIfRequired(onVC: vc, completionBlock: { guard let userEmail = EmailHelper.singleton().authorization?.userEmail, EmailHelper.singleton().authorization?.canAuthorize() ?? false else { print("Google authetication failed") - self.isGoogleAuth = false return } - self.currentIMAP = MailSession.init(configSession: .IMAP, mailAddress: userEmail.lowercased(), password: "", username: userEmail.lowercased()) - let listenerIMAP = Listener(type: SessionType.IMAP, callback: self.onImapCompleted, login: userEmail.lowercased(), password: "") - self.currentIMAP?.addListener(listener: listenerIMAP) - self.currentIMAP?.setServer(hostname: "imap.gmail.com", port: 993, connType: MCOConnectionType.TLS.rawValue, authType: MCOAuthType.xoAuth2.rawValue) - self.currentSMTP = MailSession.init(configSession: .SMTP, mailAddress: userEmail.lowercased(), password: "", username: userEmail.lowercased()) - self.currentSMTP?.setServer(hostname: "smtp.gmail.com", port: 587, connType: MCOConnectionType.startTLS.rawValue, authType: MCOAuthType.xoAuth2.rawValue) - let listenerSMTP = Listener(type: SessionType.SMTP, callback: self.onSmtpCompleted, login: userEmail.lowercased(), password: "") - self.currentSMTP?.addListener(listener: listenerSMTP) - self.checkIMAPConfig(userEmail.lowercased(), "") - }) - } - - private func setupIMAPSession(_ login: String, _ password: String, username:String? = nil) -> MailSession { - var name = login - if let n = username { - name = n - } - let mailSession = MailSession(configSession: SessionType.IMAP, mailAddress: login, password: password, username: name) - let listenerIMAP = Listener(type: SessionType.IMAP, callback: onImapCompleted, login: login, password: password) - mailSession.addListener(listener: listenerIMAP) - return mailSession - } - - private func setupSMTPSession(_ login: String, _ password: String, username:String? = nil) -> MailSession{ - var name = login - if let n = username { - name = n - } - let mailSession = MailSession(configSession: SessionType.SMTP, mailAddress: login, password: password, username: name) - let listenerSMTP = Listener(type: SessionType.SMTP, callback: onSmtpCompleted, login: login, password: password) - mailSession.addListener(listener: listenerSMTP) - return mailSession - } - - func authenticationFailed() { - previousSMTP = currentSMTP - previousIMAP = currentIMAP - currentSMTP = nil - currentIMAP = nil - - var error = MailServerConnectionError.AuthenticationError - if let imap = previousIMAP, let e = MailServerConnectionError.findPrioError(errors: imap.errors) { - error = e - } - errorMessage = error.localizedDescription - } - - private func checkDetailConfig(imap: Bool, _ login: String, _ password: String, username:String? = nil, _ imapServer:String, _ imapPort:Int, _ imapEncryption:Int, _ smtpServer:String, _ smtpPort:Int, _ smtpEncryption:Int) { - - let imapSession = setupIMAPSession(login, password) - let smtpSession = setupSMTPSession(login, password) + let googleImapPort = 993 + let googleSmtpPort = 587 + + let mailAccount = MailAccount(emailAddress: userEmail.lowercased(), password: "", username: userEmail.lowercased(), imapServer: "imap.gmail.com", imapPort: googleImapPort, imapEncryption: AuthenticationViewModel.encryptionOptions[2].value, smtpServer: "smtp.gmail.com", smtpPort: googleSmtpPort, smtpEncryption: AuthenticationViewModel.encryptionOptions[1].value, authType: MCOAuthType.xoAuth2.rawValue) - let imapConnValue = 1 << imapEncryption - let smtpConnValue = 1 << smtpEncryption - - smtpSession.setServer(hostname: smtpServer, port: UInt32(smtpPort), connType: smtpConnValue, authType: nil) - imapSession.setServer(hostname: imapServer, port: UInt32(imapPort), connType: imapConnValue, authType: nil) - - if imap { - previousSMTP = currentSMTP - previousIMAP = currentIMAP - currentSMTP = smtpSession - currentIMAP = imapSession - if imapSession.startTestingServerConfig() { - imapConfigurationSuccessful = true - } - } else { - if smtpSession.startTestingServerConfig() { - smtpConfigurationSuccessful = true - currentSMTP = smtpSession + self.showProgressSpinner = true + self.cancellable = self.model.checkConfig(mailAccount: mailAccount, extendedValidation: true).sink { promise in + switch promise { + case AuthenticationModel.AuthenticationResult.Success : + self.authenticationSucceed() + case AuthenticationModel.AuthenticationResult.Error(let value) : + self.authenticationFailed(error: value) + case AuthenticationModel.AuthenticationResult.Timeout : + self.timeoutNotification() + } + self.showProgressSpinner = false } - } + }) } - func onImapCompleted(imapWorks: Bool, _ login: String, _ password: String) { - if imapWorks { - _ = currentIMAP?.storeToUserDefaults() - checkSMTPConfig(login, password) - return - - } else if let start = startTimeIMAPCheck { - startTimeIMAPCheck = nil - let duration = abs(start.timeIntervalSinceNow) - if duration > TimeInterval(10) { - isDetailedAuthentication = true - } - } - imapConfigurationSuccessful = false - authenticationFailed() + func authenticationSucceed() { + AppDelegate.getAppDelegate().credentialsWork() } - func onSmtpCompleted(smtpWorks: Bool, _ login: String, _ password: String) { - if smtpWorks { - _ = currentSMTP?.storeToUserDefaults() - AppDelegate.getAppDelegate().credentialsWork() - return - } - smtpConfigurationSuccessful = false - authenticationFailed() + func authenticationFailed(error: MailServerConnectionError) { + errorMessage = NSLocalizedString(error.localizedUIBodyString, comment: "") } - - class Listener: MailSessionListener { - let type: SessionType - let callback: (Bool, String, String) -> Void - let login: String - let password: String - - init(type: SessionType, callback: @escaping (Bool, String, String) -> Void, login: String , password: String) { - self.type = type - self.callback = callback - self.login = login - self.password = password - } - - func testFinish(result: Bool) { - DispatchQueue.main.async(execute: { - self.callback(result, self.login, self.password) - }) - } + + func timeoutNotification() { + errorMessage = NSLocalizedString(MailServerConnectionError.TimeoutError.localizedUIBodyString, comment: "") } } diff --git a/enzevalos_iphone/MailAccount.swift b/enzevalos_iphone/MailAccount.swift new file mode 100644 index 0000000000000000000000000000000000000000..775e1154ce35e4ab0052ac41fb70a240f5f2f9be --- /dev/null +++ b/enzevalos_iphone/MailAccount.swift @@ -0,0 +1,50 @@ +// +// MailAccount.swift +// enzevalos_iphone +// +// Created by SWP Usable Security für Smartphones on 05.03.20. +// Copyright © 2020 fu-berlin. All rights reserved. +// + +import Foundation + +class MailAccount { + + let emailAddress: String + let password: String + // Username used for imap/smtp protocol. This can be different than emailAddress. Currently we do not support different usernames for imap and smtp + let username: String? + let imapServer: String + let imapPort: Int + let imapEncryption: Int + let smtpServer: String + let smtpPort: Int + let smtpEncryption: Int + let authType: Int? + + init(emailAddress: String, password: String, username: String? = nil, imapServer: String, imapPort: Int, imapEncryption: Int, smtpServer: String, smtpPort: Int, smtpEncryption: Int, authType: Int? = nil) { + self.emailAddress = emailAddress + self.password = password + self.username = username + self.imapServer = imapServer + self.imapPort = imapPort + self.imapEncryption = imapEncryption + self.smtpServer = smtpServer + self.smtpPort = smtpPort + self.smtpEncryption = smtpEncryption + self.authType = authType + } + + init(emailAddress: String, password: String, username: String? = nil) { + self.emailAddress = emailAddress + self.password = password + username == nil ? (self.username = emailAddress) : (self.username = username) + self.imapServer = "" + self.imapPort = 0 + self.imapEncryption = 0 + self.smtpServer = "" + self.smtpPort = 0 + self.smtpEncryption = 0 + self.authType = nil + } +} diff --git a/enzevalos_iphone/MailServerConnectionError.swift b/enzevalos_iphone/MailServerConnectionError.swift index d726c974c960fc6cb35cd9944ddaac2e18bc173b..5383467225e984a6101c8f0c5dee7bdaf569df90 100644 --- a/enzevalos_iphone/MailServerConnectionError.swift +++ b/enzevalos_iphone/MailServerConnectionError.swift @@ -17,7 +17,8 @@ enum UserRecommandation: Int { CheckSSLConfiguration = 4, CheckOAUTH = 5, - Default = 100 + Default = 100, + Timeout = 200 } // see: https://github.com/MailCore/mailcore2/blob/31e308a36108301e2f9cc5c4489bb81ff84e6d3a/src/objc/abstract/MCOConstants.h @@ -26,7 +27,7 @@ enum MailServerConnectionError: Error { GmailIMAPNotEnabledError, GmailExceededBandwidthLimitError, GmailTooManySimultaneousConnectionsError, MobileMeMovedError, YahooUnavailableError, ImapIdleError, IdentityError, StartTLSNotAvailableError, AuthenticationRequiredError, SMTPInvalidAccountError, ServerDateError, UnspecifiedError, StorageLimitSMTPError, NoInternetconnection, NoData, SecurityIndicatorError, - ImapSetupError, SmtpSetupError + ImapSetupError, SmtpSetupError, TimeoutError var userRecommandations: UserRecommandation { get { @@ -53,6 +54,8 @@ enum MailServerConnectionError: Error { return .CheckIMAPServerConfig case .SmtpSetupError : return .CheckSMTPServerConfig + case .TimeoutError : + return .Timeout default : return .Default } @@ -75,6 +78,8 @@ enum MailServerConnectionError: Error { return "MailServerError.SMTP.Body" case UserRecommandation.CheckConnectionInfo : return "MailServerError.Connection.Body" + case UserRecommandation.Timeout : + return "MailServerError.Timeout.Body" default: return "MailServerError.Default.Body" } diff --git a/enzevalos_iphone/MailSession.swift b/enzevalos_iphone/MailSession.swift index 1df6ce70ad5ba706546da5a92236fc7f2e42f9c3..e56b7097ea6f24d6ef3c06eddcaf0eeab09e335f 100644 --- a/enzevalos_iphone/MailSession.swift +++ b/enzevalos_iphone/MailSession.swift @@ -67,7 +67,7 @@ class MailServer: Comparable { var loggerCalled = false var works = false var sendCallback = false - + var isTLS: Bool { get{ return connectionType == MCOConnectionType.TLS @@ -502,6 +502,9 @@ class MailSession { 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 dispatchQueue: DispatchQueue = DispatchQueue(label: "MailSessionQueue") + + var defaultIMAPSession: MCOIMAPSession? { get { if let server = try? MailServer(userValues: sessionType, callback: callback) { @@ -722,20 +725,22 @@ class MailSession { } private func readJson() -> [MCONetService] { - var servers: [MCONetService] = [] - let manager = MCOMailProvidersManager.shared() - if let path = Bundle.main.path(forResource: "providers", ofType: "json") { - manager.registerProviders(withFilename: path) - if let provider = manager.provider(forEmail: mailAddr){ - if sessionType == SessionType.IMAP { - servers = provider.imapServices() - } - else { - servers = provider.smtpServices() + MailSession.dispatchQueue.sync { + var servers: [MCONetService] = [] + let manager = MCOMailProvidersManager.shared() + if let path = Bundle.main.path(forResource: "providers", ofType: "json") { + manager.registerProviders(withFilename: path) + if let provider = manager.provider(forEmail: mailAddr){ + if sessionType == SessionType.IMAP { + servers = provider.imapServices() + } + else { + servers = provider.smtpServices() + } } } + return servers } - return servers } var hasJsonFile: Bool { diff --git a/enzevalos_iphone/de.lproj/Localizable.strings b/enzevalos_iphone/de.lproj/Localizable.strings index a24c0fc65f1ed8f016d11fbf7c4771a4c86a33d8..30b1c8674f102e68532214a3f5f3e82feaca573e 100644 --- a/enzevalos_iphone/de.lproj/Localizable.strings +++ b/enzevalos_iphone/de.lproj/Localizable.strings @@ -286,6 +286,8 @@ "MailServerError.Default.Body" = "Es konnte keine Verbindung zum Server hergestellt werden.\n Bitte überprüfe das Password, den Accountname und die Serverkonfiguration (IMAP/SMTP). Die Serverkonfiguration kann mit der Serverkonfiguration auf der Provider-Webseite verglichen werden. In manchen Fällen muss der Zugriff mittels IMAP/SMTP in den Accounteinstellungen erst freigeschaltet werden."; "MailServerError.IMAP.Body" = "Es konnte keine Verbindung zum Server hergestellt werden.\n Bitte überprüfe die IMAP Serverkonfiguration."; "MailServerError.SMTP.Body" = "Es konnte keine Verbindung zum Server hergestellt werden.\n Bitte überprüfe die SMTP Serverkonfiguration."; +"MailServerError.Timeout.Body" = "Der Server hat nicht rechtzeitig geantwortet"; + "Read.Import.Secret.Title" = "Neuer geheimer Schlüssel"; "Read.Import.Secret.Body.PW" = "gib zum Importieren das Passwort für den geheimen Schlüssel mit der ID %@ ein."; diff --git a/enzevalos_iphone/en.lproj/Localizable.strings b/enzevalos_iphone/en.lproj/Localizable.strings index d497851a44be628c7fa26c92c15c8e9af8a7cc33..70ce192b65d789fc9cd116491b260014a2631a7c 100644 --- a/enzevalos_iphone/en.lproj/Localizable.strings +++ b/enzevalos_iphone/en.lproj/Localizable.strings @@ -261,6 +261,7 @@ "MailServerError.Default.Body" = "Couldn't connect to server.\n Please, check your account name, password and server configuration. You can also compare the server configuration with the configuration listed on the website of your provider. In some cases, you have to activate the IMAP/SMTP connection in your account settings."; "MailServerError.IMAP.Body" = "Couldn't connect to server.\n Please check your IMAP server configuration."; "MailServerError.SMTP.Body" = "Couldn't connect to server.\n Please check your SMTP server configuration."; +"MailServerError.Timeout.Body" = "Server didn't answer in time"; "Read.Import.Secret.Title" = "New secret"; "Read.Import.Secret.Body.PW" = "Please, enter the password to import the new secret key with the id: %@."; diff --git a/enzevalos_iphoneTests/GeneratedMocks.swift b/enzevalos_iphoneTests/GeneratedMocks.swift new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391