diff --git a/ObjectivePGP.framework/Info.plist b/ObjectivePGP.framework/Info.plist index 39de5613232d667e6792fb58a8e8b29be36ba34b..07167478d196b99ed5f2bbe3d308e80c8fee002d 100644 Binary files a/ObjectivePGP.framework/Info.plist and b/ObjectivePGP.framework/Info.plist differ diff --git a/ObjectivePGP.framework/ObjectivePGP b/ObjectivePGP.framework/ObjectivePGP index d055662492d5c48d881aa65eee82d996e9d6fc20..a8326c2d5a202d74ae14728798b1b4f77c5df0b2 100755 Binary files a/ObjectivePGP.framework/ObjectivePGP and b/ObjectivePGP.framework/ObjectivePGP differ diff --git a/enzevalos_iphone.xcodeproj/project.pbxproj b/enzevalos_iphone.xcodeproj/project.pbxproj index d3afdb029c92e8e11262f34ffc2a7f11c44cc207..df510eb6269d3dc38d9e3936650c92551c40d77a 100644 --- a/enzevalos_iphone.xcodeproj/project.pbxproj +++ b/enzevalos_iphone.xcodeproj/project.pbxproj @@ -14,10 +14,8 @@ 3EC35F2420037651008BDF95 /* InvitationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EC35F2320037651008BDF95 /* InvitationHelper.swift */; }; 3EC35F2D200376A1008BDF95 /* SendViewController+Invitation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EC35F2C200376A1008BDF95 /* SendViewController+Invitation.swift */; }; 3EC35F302003838E008BDF95 /* InvitationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EC35F2F2003838E008BDF95 /* InvitationTests.swift */; }; - 45262931B4C72A96C686C533 /* Pods_enzevalos_iphoneTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C9B9CE43043CF806E1C02FCA /* Pods_enzevalos_iphoneTests.framework */; }; 4706D65F225B7B6B00B3F1D3 /* ItunesHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4706D65E225B7B6B00B3F1D3 /* ItunesHandler.swift */; }; 4706D661225CD21D00B3F1D3 /* ExportKeyHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4706D660225CD21D00B3F1D3 /* ExportKeyHelper.swift */; }; - 4706D663225CDFE200B3F1D3 /* TempKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4706D662225CDFE200B3F1D3 /* TempKey.swift */; }; 4707091E2189BC3500DF71A3 /* plainThunderbird.eml in Resources */ = {isa = PBXBuildFile; fileRef = 470709172189BC3500DF71A3 /* plainThunderbird.eml */; }; 470709262189C73900DF71A3 /* enc+signedInlineThunderbird.eml in Resources */ = {isa = PBXBuildFile; fileRef = 470709212189C73900DF71A3 /* enc+signedInlineThunderbird.eml */; }; 470709272189C73900DF71A3 /* encThunderbird.eml in Resources */ = {isa = PBXBuildFile; fileRef = 470709222189C73900DF71A3 /* encThunderbird.eml */; }; @@ -43,6 +41,7 @@ 472F398E1E251B8D009260FB /* MailAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472F398D1E251B8D009260FB /* MailAddress.swift */; }; 472F39901E252470009260FB /* CNMailAddressesExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 472F398F1E252470009260FB /* CNMailAddressesExtension.swift */; }; 474054982244D7A9007CF83B /* MailServerConfigurationTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474054972244D7A9007CF83B /* MailServerConfigurationTest.swift */; }; + 474994022261E4E6000F8DA5 /* SimpleSendIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 474994012261E4E6000F8DA5 /* SimpleSendIcon.swift */; }; 4756DE0E20402F8E00452288 /* invitationTextCensor.html in Resources */ = {isa = PBXBuildFile; fileRef = 4756DE0D20402F8E00452288 /* invitationTextCensor.html */; }; 475B00331F7B9565006CDD41 /* SwiftPGP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475B00301F7B9565006CDD41 /* SwiftPGP.swift */; }; 475B00341F7B9565006CDD41 /* Cryptography.swift in Sources */ = {isa = PBXBuildFile; fileRef = 475B00311F7B9565006CDD41 /* Cryptography.swift */; }; @@ -80,6 +79,12 @@ 479C649621F2139B00A01071 /* support_pk.asc in Resources */ = {isa = PBXBuildFile; fileRef = 479C649521F2139B00A01071 /* support_pk.asc */; }; 479C649A21F45DAF00A01071 /* HideShowPasswordTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 479C649821F45DAF00A01071 /* HideShowPasswordTextField.swift */; }; 479C649B21F45DAF00A01071 /* PasswordToggleVisibilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 479C649921F45DAF00A01071 /* PasswordToggleVisibilityView.swift */; }; + 47A5D6D42294A8F60084F81D /* OnboardingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A5D6D32294A8F60084F81D /* OnboardingTest.swift */; }; + 47A5D6D92294B4EC0084F81D /* ObjectivePGP.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47CEF4EC2052C3E600887CDB /* ObjectivePGP.framework */; }; + 47A5D6DA2294B50E0084F81D /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 47F867E12052B48E00AA832F /* libz.tbd */; }; + 47A5D6DE2294B5480084F81D /* AppAuth.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47A5D6DD2294B5480084F81D /* AppAuth.framework */; }; + 47A5D6E22294BF3B0084F81D /* TempKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A5D6E12294BF3A0084F81D /* TempKey.swift */; }; + 47A5D6E42294BFF50084F81D /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A5D6E32294BFF50084F81D /* Logger.swift */; }; 47C22281218AFD6300BD2C2B /* AutocryptTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47C22280218AFD6300BD2C2B /* AutocryptTest.swift */; }; 47C22283218B02C700BD2C2B /* autocryptSimpleExample1.eml in Resources */ = {isa = PBXBuildFile; fileRef = 47C22282218B02C700BD2C2B /* autocryptSimpleExample1.eml */; }; 47CD5AAA2012368D00E771A1 /* logging_pk.asc in Resources */ = {isa = PBXBuildFile; fileRef = 47CD5AA82012368D00E771A1 /* logging_pk.asc */; }; @@ -102,6 +107,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 */; }; + 7500EE9D4F3130671F5C1AE2 /* Pods_enzevalos_iphoneTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7977EA7012D8E98D186D5C60 /* Pods_enzevalos_iphoneTests.framework */; }; 8428A8531F4369C0007649A5 /* Gamification.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8428A8521F4369C0007649A5 /* Gamification.storyboard */; }; 8428A8551F4369CF007649A5 /* GamificationElements.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8428A8541F4369CF007649A5 /* GamificationElements.xcassets */; }; 8428A85C1F436A05007649A5 /* ArrowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8428A8581F436A05007649A5 /* ArrowView.swift */; }; @@ -119,13 +125,11 @@ 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 */; }; - 9935BC866A86C4A4B9819F35 /* Pods_enzevalos_iphone.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AE42F42E91A1BFBF1D5BF6A /* Pods_enzevalos_iphone.framework */; }; - 9C1FA3A01B089C653802A88C /* Pods_enzevalos_iphoneUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 48FB10FF406523D174F4202A /* Pods_enzevalos_iphoneUITests.framework */; }; A102AA8A1EDDB4F40024B457 /* videoOnboarding2.m4v in Resources */ = {isa = PBXBuildFile; fileRef = A102AA891EDDB4E80024B457 /* videoOnboarding2.m4v */; }; A1083A541E8BFEA6003666B7 /* Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1083A531E8BFEA6003666B7 /* Onboarding.swift */; }; A10DAA5721F37600005D8BBB /* IntroInfoButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10DAA5621F37600005D8BBB /* IntroInfoButton.swift */; }; A10DE4201EFAA2CE005E8189 /* FolderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10DE41F1EFAA2CE005E8189 /* FolderViewController.swift */; }; - A111F6AD1FA77B170060AFDE /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = A111F6AC1FA77B170060AFDE /* Logger.swift */; }; + A111F6AD1FA77B170060AFDE /* LoggerDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = A111F6AC1FA77B170060AFDE /* LoggerDetail.swift */; }; A1123E6A1DA682850069551C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = A1123E6C1DA682850069551C /* Localizable.strings */; }; A114E4321FACB23000E40243 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A114E4311FACB23000E40243 /* StringExtension.swift */; }; A12F91D821F3A99800AB0589 /* NSLayoutConstraintExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12F91D721F3A99800AB0589 /* NSLayoutConstraintExtension.swift */; }; @@ -181,6 +185,8 @@ A1F992291DA7C9100073BF1B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A1F9922B1DA7C9100073BF1B /* Main.storyboard */; }; A1F992391DA7DD2E0073BF1B /* InboxTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = A1F9923B1DA7DD2E0073BF1B /* InboxTableViewCell.xib */; }; A1FA44A721E10E1400DB02AC /* TravelHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1FA44A621E10E1400DB02AC /* TravelHandler.swift */; }; + AC4001CA169DC07A7A1E3AD3 /* Pods_enzevalos_iphone.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 94EE54279AB591E0CAB8EFD8 /* Pods_enzevalos_iphone.framework */; }; + D630501DC9A8FA6EAD919B96 /* Pods_enzevalos_iphoneUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF232CF5EE5EE7B9838EBDF4 /* Pods_enzevalos_iphoneUITests.framework */; }; F113C3851F30D06800E7F1D6 /* QRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F113C3841F30D06800E7F1D6 /* QRScannerView.swift */; }; F113C38B1F3344C200E7F1D6 /* ViewControllerPannable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F113C38A1F3344C200E7F1D6 /* ViewControllerPannable.swift */; }; F119D2901E364B59001D732A /* AnimatedSendIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F119D28F1E364B59001D732A /* AnimatedSendIcon.swift */; }; @@ -188,7 +194,7 @@ F12041FD1DA409A5002E4940 /* ListViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12041FC1DA409A5002E4940 /* ListViewCell.swift */; }; F12060801DA540FE00F6EF37 /* RefreshControlExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F120607F1DA540FE00F6EF37 /* RefreshControlExtension.swift */; }; F12060821DA552FC00F6EF37 /* MailHandlerDelegator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F12060811DA552FC00F6EF37 /* MailHandlerDelegator.swift */; }; - F120A7D31F7937BB006D5BF1 /* (null) in Frameworks */ = {isa = PBXBuildFile; }; + F120A7D31F7937BB006D5BF1 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; }; F12D8DBB2069422A0068788E /* About.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F12D8DBD2069422A0068788E /* About.storyboard */; }; F14239C11F30A99C00998A83 /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F14239C01F30A99C00998A83 /* QRCodeGenerator.swift */; }; F1737ACB2031D7D70000312B /* StudySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = A17FDFF2202C685800F7BA89 /* StudySettings.swift */; }; @@ -249,7 +255,6 @@ 411EB2B85F99B48FFD36F966 /* Pods-enzevalos_iphoneTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-enzevalos_iphoneTests.debug.xcconfig"; path = "../enzevalos_iphone_workspace/Pods/Target Support Files/Pods-enzevalos_iphoneTests/Pods-enzevalos_iphoneTests.debug.xcconfig"; sourceTree = "<group>"; }; 4706D65E225B7B6B00B3F1D3 /* ItunesHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItunesHandler.swift; sourceTree = "<group>"; }; 4706D660225CD21D00B3F1D3 /* ExportKeyHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExportKeyHelper.swift; sourceTree = "<group>"; }; - 4706D662225CDFE200B3F1D3 /* TempKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempKey.swift; sourceTree = "<group>"; }; 470709172189BC3500DF71A3 /* plainThunderbird.eml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = plainThunderbird.eml; sourceTree = "<group>"; }; 470709212189C73900DF71A3 /* enc+signedInlineThunderbird.eml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "enc+signedInlineThunderbird.eml"; sourceTree = "<group>"; }; 470709222189C73900DF71A3 /* encThunderbird.eml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = encThunderbird.eml; sourceTree = "<group>"; }; @@ -275,6 +280,7 @@ 472F398D1E251B8D009260FB /* MailAddress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MailAddress.swift; sourceTree = "<group>"; }; 472F398F1E252470009260FB /* CNMailAddressesExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CNMailAddressesExtension.swift; sourceTree = "<group>"; }; 474054972244D7A9007CF83B /* MailServerConfigurationTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailServerConfigurationTest.swift; sourceTree = "<group>"; }; + 474994012261E4E6000F8DA5 /* SimpleSendIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleSendIcon.swift; sourceTree = "<group>"; }; 4756DE0D20402F8E00452288 /* invitationTextCensor.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; name = invitationTextCensor.html; path = Invitation/invitationTextCensor.html; sourceTree = "<group>"; }; 475B00301F7B9565006CDD41 /* SwiftPGP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftPGP.swift; sourceTree = "<group>"; }; 475B00311F7B9565006CDD41 /* Cryptography.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cryptography.swift; sourceTree = "<group>"; }; @@ -308,6 +314,12 @@ 479C649521F2139B00A01071 /* support_pk.asc */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = support_pk.asc; sourceTree = "<group>"; }; 479C649821F45DAF00A01071 /* HideShowPasswordTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HideShowPasswordTextField.swift; sourceTree = "<group>"; }; 479C649921F45DAF00A01071 /* PasswordToggleVisibilityView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordToggleVisibilityView.swift; sourceTree = "<group>"; }; + 47A5D6D32294A8F60084F81D /* OnboardingTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTest.swift; sourceTree = "<group>"; }; + 47A5D6D52294B4830084F81D /* GTMAppAuth.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = GTMAppAuth.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 47A5D6DB2294B5220084F81D /* libz.1.1.3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.1.1.3.tbd; path = usr/lib/libz.1.1.3.tbd; sourceTree = SDKROOT; }; + 47A5D6DD2294B5480084F81D /* AppAuth.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AppAuth.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 47A5D6E12294BF3A0084F81D /* TempKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TempKey.swift; sourceTree = "<group>"; }; + 47A5D6E32294BFF50084F81D /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; }; 47B2318A1F0D458100961B28 /* enzevalos_iphone 2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "enzevalos_iphone 2.xcdatamodel"; 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>"; }; @@ -331,11 +343,10 @@ 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; }; - 48FB10FF406523D174F4202A /* Pods_enzevalos_iphoneUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_enzevalos_iphoneUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 4AE42F42E91A1BFBF1D5BF6A /* Pods_enzevalos_iphone.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_enzevalos_iphone.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 66E758F271CD65AB3E5FE7A7 /* 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>"; }; 6EBCCD02AD3B95D8317810E2 /* Pods-enzevalos_iphoneTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-enzevalos_iphoneTests.debug.xcconfig"; path = "../enzevalos_iphone_workspace/Pods/Target Support Files/Pods-enzevalos_iphoneTests/Pods-enzevalos_iphoneTests.debug.xcconfig"; sourceTree = "<group>"; }; 796D16D79BED5D60B580E602 /* 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>"; }; + 7977EA7012D8E98D186D5C60 /* Pods_enzevalos_iphoneTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_enzevalos_iphoneTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8428A8521F4369C0007649A5 /* Gamification.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Gamification.storyboard; sourceTree = "<group>"; }; 8428A8541F4369CF007649A5 /* GamificationElements.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = GamificationElements.xcassets; sourceTree = "<group>"; }; 8428A8561F4369EA007649A5 /* GamificationDataUnitTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamificationDataUnitTest.swift; sourceTree = "<group>"; }; @@ -354,13 +365,14 @@ 8428A86D1F436A1E007649A5 /* GamificationStatusViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GamificationStatusViewController.swift; sourceTree = "<group>"; }; 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; }; 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>"; }; A102AA891EDDB4E80024B457 /* videoOnboarding2.m4v */ = {isa = PBXFileReference; lastKnownFileType = file; path = videoOnboarding2.m4v; sourceTree = "<group>"; }; A1083A531E8BFEA6003666B7 /* Onboarding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Onboarding.swift; sourceTree = "<group>"; }; A10DAA5621F37600005D8BBB /* IntroInfoButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroInfoButton.swift; sourceTree = "<group>"; }; A10DE41F1EFAA2CE005E8189 /* FolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FolderViewController.swift; sourceTree = "<group>"; }; - A111F6AC1FA77B170060AFDE /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; }; + A111F6AC1FA77B170060AFDE /* LoggerDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerDetail.swift; sourceTree = "<group>"; }; A1123E6B1DA682850069551C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; }; A1123E6D1DA682870069551C /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = "<group>"; }; A114E4311FACB23000E40243 /* StringExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtension.swift; sourceTree = "<group>"; }; @@ -438,7 +450,7 @@ BC7D006B3B40A23E53B4F317 /* Pods-enzevalos_iphoneTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-enzevalos_iphoneTests.release.xcconfig"; path = "../enzevalos_iphone_workspace/Pods/Target Support Files/Pods-enzevalos_iphoneTests/Pods-enzevalos_iphoneTests.release.xcconfig"; sourceTree = "<group>"; }; C1F4458FC892EBE555836F55 /* Pods_enzevalos_iphone_AdHoc.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_enzevalos_iphone_AdHoc.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C7733DFEFB7E7CFF38EC1665 /* Pods-enzevalos_iphoneTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-enzevalos_iphoneTests.release.xcconfig"; path = "../enzevalos_iphone_workspace/Pods/Target Support Files/Pods-enzevalos_iphoneTests/Pods-enzevalos_iphoneTests.release.xcconfig"; sourceTree = "<group>"; }; - C9B9CE43043CF806E1C02FCA /* Pods_enzevalos_iphoneTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_enzevalos_iphoneTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EF232CF5EE5EE7B9838EBDF4 /* Pods_enzevalos_iphoneUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_enzevalos_iphoneUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F113C3841F30D06800E7F1D6 /* QRScannerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScannerView.swift; sourceTree = "<group>"; }; F113C38A1F3344C200E7F1D6 /* ViewControllerPannable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewControllerPannable.swift; sourceTree = "<group>"; }; F119D28F1E364B59001D732A /* AnimatedSendIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimatedSendIcon.swift; sourceTree = "<group>"; }; @@ -475,9 +487,9 @@ 47F867E42052B49800AA832F /* libbz2.tbd in Frameworks */, 47F867E22052B48E00AA832F /* libz.tbd in Frameworks */, 47F867E02052B47C00AA832F /* Security.framework in Frameworks */, - F120A7D31F7937BB006D5BF1 /* (null) in Frameworks */, + F120A7D31F7937BB006D5BF1 /* BuildFile in Frameworks */, 472F396E1E14F384009260FB /* CoreData.framework in Frameworks */, - 9935BC866A86C4A4B9819F35 /* Pods_enzevalos_iphone.framework in Frameworks */, + AC4001CA169DC07A7A1E3AD3 /* Pods_enzevalos_iphone.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -488,7 +500,7 @@ 479B597A20691C0600B3944D /* libz.tbd in Frameworks */, 479B597920691BFB00B3944D /* libbz2.tbd in Frameworks */, 479B597820691BE400B3944D /* ObjectivePGP.framework in Frameworks */, - 45262931B4C72A96C686C533 /* Pods_enzevalos_iphoneTests.framework in Frameworks */, + 7500EE9D4F3130671F5C1AE2 /* Pods_enzevalos_iphoneTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -496,7 +508,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9C1FA3A01B089C653802A88C /* Pods_enzevalos_iphoneUITests.framework in Frameworks */, + 47A5D6DE2294B5480084F81D /* AppAuth.framework in Frameworks */, + 47A5D6DA2294B50E0084F81D /* libz.tbd in Frameworks */, + 47A5D6D92294B4EC0084F81D /* ObjectivePGP.framework in Frameworks */, + D630501DC9A8FA6EAD919B96 /* Pods_enzevalos_iphoneUITests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -677,15 +692,18 @@ 78280F99990BFF65543B7F0B /* Frameworks */ = { isa = PBXGroup; children = ( + 47A5D6DD2294B5480084F81D /* AppAuth.framework */, + 47A5D6DB2294B5220084F81D /* libz.1.1.3.tbd */, + 47A5D6D52294B4830084F81D /* GTMAppAuth.framework */, 47CEF4EC2052C3E600887CDB /* ObjectivePGP.framework */, 47F867E32052B49800AA832F /* libbz2.tbd */, 47F867E12052B48E00AA832F /* libz.tbd */, 47F867DF2052B47C00AA832F /* Security.framework */, 472F396D1E14F384009260FB /* CoreData.framework */, - 4AE42F42E91A1BFBF1D5BF6A /* Pods_enzevalos_iphone.framework */, - C9B9CE43043CF806E1C02FCA /* Pods_enzevalos_iphoneTests.framework */, - 48FB10FF406523D174F4202A /* Pods_enzevalos_iphoneUITests.framework */, C1F4458FC892EBE555836F55 /* Pods_enzevalos_iphone_AdHoc.framework */, + 94EE54279AB591E0CAB8EFD8 /* Pods_enzevalos_iphone.framework */, + 7977EA7012D8E98D186D5C60 /* Pods_enzevalos_iphoneTests.framework */, + EF232CF5EE5EE7B9838EBDF4 /* Pods_enzevalos_iphoneUITests.framework */, ); name = Frameworks; sourceTree = "<group>"; @@ -750,7 +768,8 @@ A111F6AB1FA77AF80060AFDE /* Logging */ = { isa = PBXGroup; children = ( - A111F6AC1FA77B170060AFDE /* Logger.swift */, + A111F6AC1FA77B170060AFDE /* LoggerDetail.swift */, + 47A5D6E32294BFF50084F81D /* Logger.swift */, A18E7D761FBDE5D9002F7CC9 /* LoggingEventType.swift */, ); name = Logging; @@ -760,8 +779,8 @@ isa = PBXGroup; children = ( 475B00301F7B9565006CDD41 /* SwiftPGP.swift */, - 4706D662225CDFE200B3F1D3 /* TempKey.swift */, 476801DA218436B600F7F259 /* Autocrypt.swift */, + 47A5D6E12294BF3A0084F81D /* TempKey.swift */, 475B00311F7B9565006CDD41 /* Cryptography.swift */, 475B00321F7B9565006CDD41 /* CryptoObject.swift */, ); @@ -862,6 +881,7 @@ isa = PBXGroup; children = ( A135269B1D955BE000D3BFE1 /* enzevalos_iphoneUITests.swift */, + 47A5D6D32294A8F60084F81D /* OnboardingTest.swift */, A135269D1D955BE000D3BFE1 /* Info.plist */, ); path = enzevalos_iphoneUITests; @@ -988,6 +1008,7 @@ children = ( A1EB057F1D956851008659C1 /* SendViewController.swift */, F119D28F1E364B59001D732A /* AnimatedSendIcon.swift */, + 474994012261E4E6000F8DA5 /* SimpleSendIcon.swift */, A1EB057D1D956848008659C1 /* VENDataDelegate.swift */, A1EB05811D95685B008659C1 /* CollectionDataDelegate.swift */, A1EB05831D956867008659C1 /* TableViewDataDelegate.swift */, @@ -1078,9 +1099,9 @@ A13526711D955BDF00D3BFE1 /* Sources */, A13526721D955BDF00D3BFE1 /* Frameworks */, A13526731D955BDF00D3BFE1 /* Resources */, - 1B3A7EC4B7EC43108D4CA42F /* [CP] Embed Pods Frameworks */, 47F867DB2052B33C00AA832F /* CopyFiles */, 47F867E52052B4B500AA832F /* ShellScript */, + 3992B0CB6412E8526773B814 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -1268,11 +1289,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1B3A7EC4B7EC43108D4CA42F /* [CP] Embed Pods Frameworks */ = { + 3992B0CB6412E8526773B814 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-enzevalos_iphone/Pods-enzevalos_iphone-frameworks.sh", "${BUILT_PRODUCTS_DIR}/AppAuth/AppAuth.framework", @@ -1287,6 +1310,8 @@ "${BUILT_PRODUCTS_DIR}/VENTokenField/VENTokenField.framework", ); name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + ); outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AppAuth.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/BZipCompression.framework", @@ -1396,6 +1421,7 @@ 8428A8651F436A11007649A5 /* BadgeCaseCollectionViewCell.swift in Sources */, 472F39811E1E5347009260FB /* Mail_Address+CoreDataClass.swift in Sources */, A1EB05821D95685B008659C1 /* CollectionDataDelegate.swift in Sources */, + 47A5D6E22294BF3B0084F81D /* TempKey.swift in Sources */, 47D1302B1F7CEE6D007B14DF /* DebugSettings.swift in Sources */, A1EB05801D956851008659C1 /* SendViewController.swift in Sources */, 479C649B21F45DAF00A01071 /* PasswordToggleVisibilityView.swift in Sources */, @@ -1420,6 +1446,7 @@ A1735DFA205AB88500B336DB /* SendViewState.swift in Sources */, 475B00331F7B9565006CDD41 /* SwiftPGP.swift in Sources */, 477548E421F77BA0000B22A8 /* StudyParameterProtocol.swift in Sources */, + 47A5D6E42294BFF50084F81D /* Logger.swift in Sources */, 3EB4FAA420120096001D0625 /* DialogOption.swift in Sources */, F14239C11F30A99C00998A83 /* QRCodeGenerator.swift in Sources */, 478154A921FF3FF400A931EC /* Invitation.swift in Sources */, @@ -1467,8 +1494,7 @@ A1EB05961D956939008659C1 /* InboxTableViewCell.swift in Sources */, 47F79240203492E3005E7DB6 /* KeyRecord+CoreDataClass.swift in Sources */, A1083A541E8BFEA6003666B7 /* Onboarding.swift in Sources */, - A111F6AD1FA77B170060AFDE /* Logger.swift in Sources */, - 4706D663225CDFE200B3F1D3 /* TempKey.swift in Sources */, + A111F6AD1FA77B170060AFDE /* LoggerDetail.swift in Sources */, A13526791D955BDF00D3BFE1 /* AppDelegate.swift in Sources */, 476916A2216B86CF00491527 /* EnzevalosContact+CoreDataClass.swift in Sources */, A1ECE54B1EFBE7ED0009349F /* FolderCell.swift in Sources */, @@ -1489,6 +1515,7 @@ A198D2292056B384004CC838 /* SendViewDelegate.swift in Sources */, 479011492289975D0057AB04 /* NoSecIconStyleKit.swift in Sources */, F12060821DA552FC00F6EF37 /* MailHandlerDelegator.swift in Sources */, + 474994022261E4E6000F8DA5 /* SimpleSendIcon.swift in Sources */, A12F91D821F3A99800AB0589 /* NSLayoutConstraintExtension.swift in Sources */, A18E7D771FBDE5D9002F7CC9 /* LoggingEventType.swift in Sources */, F1984D741E1E92B300804E1E /* LabelStyleKit.swift in Sources */, @@ -1525,6 +1552,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 47A5D6D42294A8F60084F81D /* OnboardingTest.swift in Sources */, A135269C1D955BE000D3BFE1 /* enzevalos_iphoneUITests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1898,6 +1926,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = 66E758F271CD65AB3E5FE7A7 /* Pods-enzevalos_iphoneUITests.debug.xcconfig */; buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)_workspace", + ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; INFOPLIST_FILE = enzevalos_iphoneUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -1917,6 +1949,10 @@ isa = XCBuildConfiguration; baseConfigurationReference = 8B87EFB6CEAA31452F744015 /* Pods-enzevalos_iphoneUITests.release.xcconfig */; buildSettings = { + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)_workspace", + ); GCC_SYMBOLS_PRIVATE_EXTERN = NO; INFOPLIST_FILE = enzevalos_iphoneUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; diff --git a/enzevalos_iphone.xcodeproj/xcshareddata/xcschemes/enzevalos_iphone.xcscheme b/enzevalos_iphone.xcodeproj/xcshareddata/xcschemes/enzevalos_iphone.xcscheme index 0976c58d9bd61995247832d1f92617f2a0b2b0be..eab64b2c429453d2ca9a20593c611ac6b6f0555d 100644 --- a/enzevalos_iphone.xcodeproj/xcshareddata/xcschemes/enzevalos_iphone.xcscheme +++ b/enzevalos_iphone.xcodeproj/xcshareddata/xcschemes/enzevalos_iphone.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + codeCoverageEnabled = "YES" shouldUseLaunchSchemeArgsEnv = "YES"> <Testables> <TestableReference @@ -64,6 +65,11 @@ </BuildableReference> </MacroExpansion> <AdditionalOptions> + <AdditionalOption + key = "NSZombieEnabled" + value = "YES" + isEnabled = "YES"> + </AdditionalOption> </AdditionalOptions> </TestAction> <LaunchAction diff --git a/enzevalos_iphone/AnimatedSendIcon.swift b/enzevalos_iphone/AnimatedSendIcon.swift index 79a93830e4eccd4a59a7c43adb25663c317361ae..196d76bc2940c49d1270665f537073322e1c365a 100644 --- a/enzevalos_iphone/AnimatedSendIcon.swift +++ b/enzevalos_iphone/AnimatedSendIcon.swift @@ -22,7 +22,7 @@ import Foundation import UIKit -class AnimatedSendIcon: UIView { +class AnimatedSendIcon: UIView, SendIcon { var isPostcardOnTop = false var square = UIImageView(image: StudySettings.securityIndicator.imageOfSecureIndicator(background: true, open: false)) var square2 = UIImageView(image: StudySettings.securityIndicator.imageOfInsecureIndicator(background: true)) diff --git a/enzevalos_iphone/AppDelegate.swift b/enzevalos_iphone/AppDelegate.swift index 69dd886f71b21f48eb8bdb4a3015bbac062cbf57..47b756d4a87e9f6c2f098164a494567ce4d5aede 100644 --- a/enzevalos_iphone/AppDelegate.swift +++ b/enzevalos_iphone/AppDelegate.swift @@ -50,7 +50,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { //StudySettings.firstMail() if (!UserDefaults.standard.bool(forKey: "launchedBefore")) { // Logger.queue.async(flags: .barrier) { - Logger.log(startApp: true) + // Logger.log(startApp: true) // } // Remove Google Auth token from keychain GTMKeychain.removePasswordFromKeychain(forName: "googleOAuthCodingKey") @@ -67,7 +67,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } else { AddressHandler.updateCNContacts() - Logger.log(startApp: false) + // Logger.log(startApp: false) presentInboxViewController() } NotificationCenter.default.addObserver( @@ -114,7 +114,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func googleLogin(vc: UIViewController) { - Logger.log(onboardingState: "oAuth") + // Logger.log(onboardingState: .GoogleLogIn) + Logger.log(onboardingState: .GoogleLogIn, duration: 0) if self.currentReachabilityStatus == .notReachable { if showAlertNoConnection { let alert = UIAlertController(title: NSLocalizedString("Error.noInternet.Title", comment: ""), message: NSLocalizedString("Error.noInternet.Message", comment: ""), preferredStyle: .alert) @@ -215,7 +216,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } func onboardingDone() { - Logger.log(onboardingState: "done") + Logger.log(onboardingState: .Finish, duration: 0) UserDefaults.standard.set(true, forKey: "launchedBefore") self.window?.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() presentInboxViewController() @@ -352,6 +353,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func hasNewMails(_ newMails: UInt32, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void){ let duration = Date().timeIntervalSince(start) Logger.log(backgroundFetch: newMails, duration: duration) + + guard newMails > 0 else { + UIApplication.shared.applicationIconBadgeNumber = 0 + completionHandler(.noData) + return + } let mails = newMails - 1 if mails > 0 { UIApplication.shared.applicationIconBadgeNumber = Int(mails) diff --git a/enzevalos_iphone/Autocrypt.swift b/enzevalos_iphone/Autocrypt.swift index 651569507a6e104ca765907fbbb5896e284cd906..e2a4b5d17e12688ebe8070e885447d8eea664995 100644 --- a/enzevalos_iphone/Autocrypt.swift +++ b/enzevalos_iphone/Autocrypt.swift @@ -110,7 +110,7 @@ class Autocrypt { if let key = pgp.exportKey(id: id, isSecretkey: false, autocrypt: true) { var string = "\(ADDR)=" + adr string = string + "; \(ENCRYPTION)=\(encPref.name)" - string = string + "; \(KEY)= \n" + key + string = string + "; \(KEY)=" + key builder.header.setExtraHeaderValue(string, forName: AUTOCRYPTHEADER) } } diff --git a/enzevalos_iphone/CryptoObject.swift b/enzevalos_iphone/CryptoObject.swift index ebc237dc43a4466aa00b6c7335db5f535c7885df..a295183e46b763cef459c77f4cd58a1305446a1a 100644 --- a/enzevalos_iphone/CryptoObject.swift +++ b/enzevalos_iphone/CryptoObject.swift @@ -3,30 +3,31 @@ // ObjectivePGP // // Created by Oliver Wiese on 25.09.17. -// Copyright © 2017 Marcin Krzyżanowski. All rights reserved. // import Foundation -/* enum SignatureState: Int16 { - case NoSignature = 0 - case NoPublicKey = -1 - case InvalidSignature = -2 - case ValidSignature = 1 -} - -enum EncryptionState: Int16 { - case NoEncryption = 0 - case UnableToDecrypt = -1 - case ValidEncryptedWithOldKey = 1 - case ValidedEncryptedWithCurrentKey = 2 -} */ - enum SignatureState: Int16 { case NoSignature = 0 case NoPublicKey = 1 case InvalidSignature = -1 case ValidSignature = 2 + + var name: String { + get { + switch self { + case .NoSignature: + return "NoSignature" + case .NoPublicKey: + return "NoPublicKey" + case .InvalidSignature: + return "InvalidSignature" + case .ValidSignature: + return "ValidSignature" + } + } + } + public static let allStates = [NoSignature, NoPublicKey, InvalidSignature, ValidSignature] } enum EncryptionState: Int16 { @@ -34,6 +35,22 @@ enum EncryptionState: Int16 { case UnableToDecrypt = 1 case ValidEncryptedWithOldKey = 2 case ValidedEncryptedWithCurrentKey = 3 + + var name: String { + get { + switch self { + case .NoEncryption: + return "NoEnc" + case .UnableToDecrypt: + return "UnableToDec" + case .ValidedEncryptedWithCurrentKey: + return "DecWithPrefKey" + case .ValidEncryptedWithOldKey: + return "DecWithNoPrefKey" + } + } + } + public static let allStates = [NoEncryption, UnableToDecrypt, ValidEncryptedWithOldKey, ValidedEncryptedWithCurrentKey] } public enum CryptoScheme { diff --git a/enzevalos_iphone/DataHandler.swift b/enzevalos_iphone/DataHandler.swift index 40310396723312aa9ffbdbc9ed79d9cdb9ce1c42..effea58adc6e5cd4b8867d97bfa57e14f2c8b183 100644 --- a/enzevalos_iphone/DataHandler.swift +++ b/enzevalos_iphone/DataHandler.swift @@ -377,11 +377,11 @@ class DataHandler { search.addToMailaddresses(adr) pk = search if Logger.logging { - var importChannel = "autocrypt" + var importChannel = LogData.TransferType.autocrypt if newGenerated { - importChannel = "generated" + importChannel = LogData.TransferType.generated } else if !autocrypt { - importChannel = "attachment" + importChannel = .mail } Logger.log(discover: pk.keyID, mailAddress: adr, importChannel: importChannel, knownPrivateKey: DataHandler.handler.findSecretKeys().map { ($0.keyID ?? "") == keyID }.reduce(false, { $0 || $1 }), knownBefore: true) } @@ -410,11 +410,11 @@ class DataHandler { save(during: "new pk") } if Logger.logging { - var importChannel = "autocrypt" + var importChannel = LogData.TransferType.autocrypt if newGenerated { - importChannel = "generated" + importChannel = .generated } else if !autocrypt { - importChannel = "attachment" + importChannel = .mail } Logger.log(discover: pk.keyID, mailAddress: adr, importChannel: importChannel, knownPrivateKey: DataHandler.handler.findSecretKeys().map { ($0.keyID ?? "") == keyID }.reduce(false, { $0 || $1 }), knownBefore: false) } @@ -431,6 +431,7 @@ class DataHandler { if saveKey { save(during: "new PK") } + adr.addToKeys(key: pk) return pk } @@ -469,6 +470,13 @@ class DataHandler { } return [SecretKey]() } + + func findPublicKeys() -> [PersistentKey] { + if let result = findAll("PersistentKey") { + return result as! [PersistentKey] + } + return [PersistentKey]() + } func findSecretKey(keyID: String) -> SecretKey? { if let result = find("SecretKey", type: "keyID", search: keyID) { @@ -516,6 +524,14 @@ class DataHandler { private func findAll(_ entityName: String) -> [AnyObject]? { let fReq: NSFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName) + if entityName == "KeyRecord" { + let sortDescriptor = NSSortDescriptor(key: "newestDate", ascending: false) + fReq.sortDescriptors = [sortDescriptor] + } + else if entityName == "PersistentMail" { + let sortDescriptor = NSSortDescriptor(key: "date", ascending: false) + fReq.sortDescriptors = [sortDescriptor] + } let result: [AnyObject]? do { result = try self.managedObjectContext.fetch(fReq) @@ -654,6 +670,25 @@ class DataHandler { } return nil } + + func findMailAddress(withKey: Bool) -> [Mail_Address] { + let fReq: NSFetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "Mail_Address") + if withKey { + fReq.predicate = NSPredicate(format: "keys.@count > 0") + } + else { + fReq.predicate = NSPredicate(format: "keys.@count == 0") + } + do { + let result = try self.managedObjectContext.fetch(fReq) + if let res = result as? [Mail_Address] { + return res + } + } catch _ as NSError { + print("error") + } + return [Mail_Address]() + } func getMailAddressByMCOAddress(_ address: MCOAddress, temporary: Bool) -> MailAddress { @@ -781,6 +816,27 @@ class DataHandler { } return nil } + + func removeAttachments() -> Int { + var safedStorage = 0 + var counterAttachments = 0 + var counterManipulatedMails = 0 + for m in getAllPersistentMails() { + if m.hasAttachment { + counterManipulatedMails += 1 + if let atts = m.attachments { + counterAttachments += atts.count + for x in atts { + if let data = x as? Attachment { + safedStorage += data.data?.count ?? 0 + m.removeFromAttachments(data) + } + } + } + } + } + return safedStorage + } // -------- End handle to, cc, from addresses -------- func createMail(_ uid: UInt64, sender: MCOAddress?, receivers: [MCOAddress], cc: [MCOAddress], time: Date, received: Bool, subject: String, body: String?, readableAttachments: Set<TempAttachment> = Set<TempAttachment>(), flags: MCOMessageFlag, record: KeyRecord?, autocrypt: Autocrypt?, decryptedData: CryptoObject?, folderPath: String, secretKey: String?, references: [String] = [], mailagent: String? = nil, messageID: String? = nil, encryptedBody: String?, storeEncrypted: Bool = false) -> PersistentMail? { @@ -794,7 +850,7 @@ class DataHandler { mails = tmpMails } - if finding == nil || finding!.count == 0 || mails.filter({ $0.folder.path == folderPath && $0.uidvalidity == myfolder.uidvalidity }).count == 0 || uid == 0{ + if finding == nil || finding!.count == 0 || mails.filter({ $0.folder.path == folderPath && $0.uidvalidity == myfolder.uidvalidity }).count == 0 || uid == 0 || !myfolder.uids.contains(uid) { // create new mail object mail = NSEntityDescription.insertNewObject(forEntityName: "PersistentMail", into: managedObjectContext) as! PersistentMail @@ -924,6 +980,14 @@ class DataHandler { } record.addToPersistentMails(mail) mail.folder.addToKeyRecords(record) + if record.newestDate == nil { + record.newestDate = mail.date + } + if let date = record.newestDate, date.timeIntervalSince(mail.date) < 0 { + // We should update our records... + record.newestDate = mail.date + } + for at in readableAttachments{ _ = self.newAttachment(temp: at, mail: mail) } @@ -941,7 +1005,6 @@ class DataHandler { } } return adrs - } func getContacts() -> [EnzevalosContact] { @@ -973,9 +1036,12 @@ class DataHandler { func getAllKeyRecords() -> [KeyRecord] { var result: [KeyRecord] = [] - if let folders = findAll("Folder") as? [Folder] { - for folder in folders { - result += folder.records + if let records = findAll("KeyRecord") as? [KeyRecord] { + result = records.filter{ + if let mails = $0.persistentMails { + return mails.count != 0 + } + return false } } return result diff --git a/enzevalos_iphone/Folder+CoreDataClass.swift b/enzevalos_iphone/Folder+CoreDataClass.swift index 2762bcf51c86f82cc6dcd7a58d704609ce147091..eb00228747ca780a931824839abb5765c099f305 100644 --- a/enzevalos_iphone/Folder+CoreDataClass.swift +++ b/enzevalos_iphone/Folder+CoreDataClass.swift @@ -40,9 +40,7 @@ public class Folder: NSManagedObject { get { let ids = MCOIndexSet() - for m in mailsOfFolder { - ids.add(m.uid) - } + mailsOfFolder.forEach{ids.add($0.uid)} return ids } } @@ -51,12 +49,11 @@ public class Folder: NSManagedObject { var records: [KeyRecord] { get { - if let keyRecords = keyRecords as? Set<KeyRecord> { - return Array(keyRecords).sorted() + guard keyRecords != nil && keyRecords?.count ?? 0 > 0 else { + return [] } - return [] + return DataHandler.handler.getAllKeyRecords() } - } @@ -86,60 +83,4 @@ public class Folder: NSManagedObject { return folders } } - - /* - func updateRecords(mail: PersistentMail){ - if let reccords = storedRecords{ - if reccords.count <= 2{ - updateRecords() - return - } - var founded = false - for i in 1..<reccords.count { - let r = reccords[i] - if r.match(mail: mail){ - founded = true - if r.mailsInFolder(folder: self).first == mail { - if reccords[i-1] > r { - storedRecords?.sort() - break - } - } - } - } - if !founded && mail.folder == self{ - if mail.isSecure && mail.keyID != nil{ - let record = KeyRecord(keyID: mail.keyID!, folder: self) - if !(storedRecords?.contains(record))!{ - if record.mails.count > 0{ - storedRecords?.append(record) - } - } - } - else { - let record = KeyRecord(contact: mail.from.contact!, folder: self) - if !(storedRecords?.contains(record))!{ - if record.mails.count > 0{ - storedRecords?.append(record) - } - } - } - storedRecords?.sort() - } - } - else{ - if mail.folder == self{ - storedRecords = [KeyRecord]() - if mail.isSecure && mail.keyID != nil{ - let record = KeyRecord(keyID: mail.keyID!, folder: self) - storedRecords?.append(record) - } - else{ - let record = KeyRecord(contact: mail.from.contact!, folder: self) - storedRecords?.append(record) - } - } - } - } - */ } diff --git a/enzevalos_iphone/Folder+CoreDataProperties.swift b/enzevalos_iphone/Folder+CoreDataProperties.swift index f83885de0188b633bfc81164cc6d8d0dcee317a3..a1f45577d6d09965f49de8b8b165a177a8ead9b8 100644 --- a/enzevalos_iphone/Folder+CoreDataProperties.swift +++ b/enzevalos_iphone/Folder+CoreDataProperties.swift @@ -83,6 +83,29 @@ extension Folder { return text! } } + + public var minID: UInt64 { + set { + self.willChangeValue(forKey: "minUID") + self.setPrimitiveValue(NSDecimalNumber.init(value: newValue as UInt64), forKey: "minUID") + self.didChangeValue(forKey: "minUID") + } + get { + self.willAccessValue(forKey: "minUID") + let id = (self.primitiveValue(forKey: "minUID") as? NSDecimalNumber)?.uint64Value + self.didAccessValue(forKey: "minUID") + if id == nil { + var min = maxID + mailsOfFolder.forEach{ + if $0.uid < min { + min = $0.uid + } + } + return min + } + return id! + } + } } // MARK: Generated accessors for mails diff --git a/enzevalos_iphone/FolderViewController.swift b/enzevalos_iphone/FolderViewController.swift index 115a327b1e41c98fe483d243d75fff969496b1e7..c7dee764c99661711fc8a675850ae78e19c96454 100644 --- a/enzevalos_iphone/FolderViewController.swift +++ b/enzevalos_iphone/FolderViewController.swift @@ -22,6 +22,7 @@ import UIKit class FolderViewController: UITableViewController { + var mailHandler = AppDelegate.getAppDelegate().mailHandler var folders: [Folder] = [] var isFirstFolderViewController = true @@ -54,7 +55,7 @@ class FolderViewController: UITableViewController { if let thisFolder = presentedFolder { navigationItem.title = UserManager.convertToFrontendFolderPath(from: thisFolder.name) refreshControl?.beginRefreshing() - AppDelegate.getAppDelegate().mailHandler.updateFolder(folder: thisFolder, completionCallback: endRefreshing) + mailHandler.updateFolder(folder: thisFolder, completionCallback: endRefreshing) folders = thisFolder.subfolders.sorted() } NotificationCenter.default.addObserver(forName: Notification.Name.NSManagedObjectContextDidSave, object: nil, queue: nil, using: { @@ -203,7 +204,7 @@ class FolderViewController: UITableViewController { lastUpdateText = NSLocalizedString("Updating", comment: "Getting new data") if let thisFolder = presentedFolder { refreshControl?.beginRefreshing() - AppDelegate.getAppDelegate().mailHandler.updateFolder(folder: thisFolder, completionCallback: endRefreshing(_:)) + mailHandler.updateFolder(folder: thisFolder, completionCallback: endRefreshing(_:)) } else { DataHandler.handler.callForFolders(done: endRefreshing) } diff --git a/enzevalos_iphone/ItunesHandler.swift b/enzevalos_iphone/ItunesHandler.swift index ae20a41e8e07700e1641e5a3d6a98d514ab98e67..4ffabfb176e9eed0fb86660b04dfeb3a3f3a0163 100644 --- a/enzevalos_iphone/ItunesHandler.swift +++ b/enzevalos_iphone/ItunesHandler.swift @@ -48,7 +48,7 @@ class ItunesKeyHandling { get { var keys: [TempKey] = [] for url in keyURL { - if let file = try? FileHandle(forReadingFrom: url){ + if let file = try? FileHandle(forReadingFrom: url) { let data = file.readDataToEndOfFile() var creationDate = Date() if let attrs = try? fileManager.attributesOfItem(atPath: url.path) as NSDictionary, let date = attrs.fileCreationDate() { @@ -68,7 +68,6 @@ class ItunesKeyHandling { var keys: [URL] = [] let dirs = fileManager.urls(for: .documentDirectory, in: .userDomainMask) for url in dirs { - print(url) if let files = try? fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) { let keyFiles = files.filter{ $0.pathExtension == "asc"} keys.append(contentsOf: keyFiles) diff --git a/enzevalos_iphone/KeyRecord+CoreDataProperties.swift b/enzevalos_iphone/KeyRecord+CoreDataProperties.swift index 28689eed9dfa8202295ccc48886e46b088bc351e..ab33fe7478d78c954c12133cdac00f53ae28ecda 100644 --- a/enzevalos_iphone/KeyRecord+CoreDataProperties.swift +++ b/enzevalos_iphone/KeyRecord+CoreDataProperties.swift @@ -32,6 +32,7 @@ extension KeyRecord { @NSManaged public var folder: Folder? @NSManaged public var key: PersistentKey? @NSManaged public var persistentMails: NSSet? + @NSManaged public var newestDate: Date? var activeKey: PersistentKey? { get { diff --git a/enzevalos_iphone/Logger.swift b/enzevalos_iphone/Logger.swift index edec43b402338f04c03f58515e5aed63bd42b49a..9a34558dedee86d5dbf8270368648a94d5d1dba9 100644 --- a/enzevalos_iphone/Logger.swift +++ b/enzevalos_iphone/Logger.swift @@ -1,40 +1,21 @@ // -// Logger.swift +// LoggerFirstStudy.swift // enzevalos_iphone // -// Created by jakobsbode on 25.10.17. -// Copyright © 2018 fu-berlin. -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see <https://www.gnu.org/licenses/>. +// Created by Oliver Wiese on 09.05.19. +// Copyright © 2019 fu-berlin. All rights reserved. // import Foundation -class Logger { - - static var logging = false //StudySettings.studyMode - - static let queue = DispatchQueue(label: "logging", qos: .background) - - static let defaultFileName = "log.json" +class Logger{ + + static var logging = StudySettings.studyMode static let loggingInterval = 21600 //60*60*6 seconds = 6 hours static let resendInterval = 5 * 60 static let logReceiver = LOGGING_MAIL_ADR - + static var nextDeadline = (UserManager.loadUserValue(Attribute.nextDeadline) as? Date) ?? Date() - - static var studyID = StudySettings.studyID //identifies the participant in the study - static fileprivate func sendCheck() { @@ -46,903 +27,635 @@ class Logger { sendLog() } } - - static fileprivate func plainLogDict() -> [String: Any] { - var fields: [String: Any] = [:] - let now = Date() - fields["timestamp"] = now.description - fields["lang"] = Locale.current.languageCode - return fields - } - - static fileprivate func dictToJSON(fields: [String: Any]) -> String { - do { - let jsonData = try JSONSerialization.data(withJSONObject: fields) - if let json = String(data: jsonData, encoding: String.Encoding.utf8) { - return json - } - return "" - } catch { - return "{\"error\":\"json conversion failed\"}" - } - } - + static func log(setupStudy studypara: [StudyParameterProtocol.Type], alreadyRegistered: Bool) { if !logging { return } - var event = plainLogDict() - event["type"] = LoggingEventType.setupStudy.rawValue - - //event["language"] = - - for para in studypara{ - event[para.name] = para.load().value - } - event["alreadyRegistered"] = alreadyRegistered - saveToDisk(json: dictToJSON(fields: event)) sendCheck() } - + static func log(startApp onboarding: Bool) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.appStart.rawValue - event["onboarding"] = onboarding - saveToDisk(json: dictToJSON(fields: event)) + + LogData.newSimpleEvent(event: .newSession) sendCheck() } - + static func log(terminateApp: Void) { if !logging { return } - var event = plainLogDict() - event["type"] = LoggingEventType.appTerminate.rawValue - saveToDisk(json: dictToJSON(fields: event)) } - + static func log(background goto: Bool) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.appBackground.rawValue - event["going to"] = goto //true -> goto background; false -> comming from background - saveToDisk(json: dictToJSON(fields: event)) + if !goto { + LogData.newSimpleEvent(event: .newSession) + } sendCheck() } - - static func log(onboardingState onboardingSection: String) { + + + static func log(onboardingState onboardingSection: IntroState, duration: Double) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.onboardingState.rawValue - event["onboardingSection"] = onboardingSection - saveToDisk(json: dictToJSON(fields: event)) + LogData.viewOnboardingSlide(type: onboardingSection, duration: duration) sendCheck() } - + static func log(onboardingPageTransition from: Int, to: Int, onboardingSection: String) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.onboardingPageTransition.rawValue - event["from"] = from - event["to"] = to - event["onboardingSection"] = onboardingSection - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(contactViewOpen keyRecord: KeyRecord?, otherRecords: [KeyRecord]?, isUser: Bool) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.contactViewOpen.rawValue - - if let keyRecord = keyRecord { - if let keyID = keyRecord.keyID { - event["keyID"] = resolve(keyID: keyID) - } else { - event["keyID"] = "nil" - } - event["mailaddresses"] = resolve(mailAddresses: keyRecord.addresses) - } - event["isUser"] = isUser - if isUser { - let (contact, mail) = GamificationData.sharedInstance.getSecureProgress() - event["gamificationContact"] = contact - event["gamificationMail"] = mail - } - event["numberOfOtherRecords"] = (otherRecords ?? []).count - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(badgeCaseViewOpen badges: [Badges]) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.badgeCaseViewOpen.rawValue - - var achievedBadges: [String] = [] - var missingBadges: [String] = [] - - for badge in badges { - if badge.isAchieved() { - achievedBadges.append(badge.displayName) - } else { - missingBadges.append(badge.displayName) - } - } - - event["achievedBadges"] = achievedBadges - event["missingBadges"] = missingBadges - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(badgeCaseViewClose badges: [Badges]) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.badgeCaseViewClose.rawValue - - var achievedBadges: [String] = [] - var missingBadges: [String] = [] - - for badge in badges { - if badge.isAchieved() { - achievedBadges.append(badge.displayName) - } else { - missingBadges.append(badge.displayName) - } - } - - event["achievedBadges"] = achievedBadges - event["missingBadges"] = missingBadges - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(contactViewClose keyRecord: KeyRecord?, otherRecords: [KeyRecord]?, isUser: Bool) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.contactViewClose.rawValue - - if let keyRecord = keyRecord { - if let keyID = keyRecord.keyID { - event["keyID"] = resolve(keyID: keyID) - } else { - event["keyID"] = "nil" - } - event["mailaddresses"] = resolve(mailAddresses: keyRecord.addresses) - } - event["isUser"] = isUser - if isUser { - let (contact, mail) = GamificationData.sharedInstance.getSecureProgress() - event["gamificationContact"] = contact - event["gamificationMail"] = mail - } - event["numberOfOtherRecords"] = (otherRecords ?? []).count - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(keyViewOpen keyID: String) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.keyViewOpen.rawValue - event["keyID"] = resolve(keyID: keyID) - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() + LogData.newSimpleEvent(event: .keyDetailsView) + } - + static func log(keyViewClose keyID: String, secondsOpened: Int) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.keyViewClose.rawValue - event["keyID"] = resolve(keyID: keyID) - event["opened for seconds"] = secondsOpened - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(exportKeyViewOpen view: Int) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.exportKeyViewOpen.rawValue - event["view"] = view - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(exportKeyViewClose view: Int) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.exportKeyViewClose.rawValue - event["view"] = view - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(exportKeyViewButton send: Bool) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.exportKeyViewClose.rawValue if send { - event["case"] = "sendMail" - } else { - event["case"] = "deletePasscode" + LogData.exportKey(keyType: .secretKey, transferType: .autocrypt) } - - saveToDisk(json: dictToJSON(fields: event)) sendCheck() } - + static func log(importPrivateKeyPopupOpen mail: PersistentMail?) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.importPrivateKeyPopupOpen.rawValue - if let mail = mail { - event = extract(from: mail, event: event) - } - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(importPrivateKeyPopupClose mail: PersistentMail?, doImport: Bool) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.importPrivateKeyPopupClose.rawValue - event["doImport"] = doImport - if let mail = mail { - event = extract(from: mail, event: event) - } - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(importPrivateKey mail: PersistentMail?, success: Bool) { if !logging { return } + LogData.importKey(keyType: .secretKey, transferType: .mail, known: false) - var event = plainLogDict() - event["type"] = LoggingEventType.importPrivateKey.rawValue - event["success"] = success - if let mail = mail { - event = extract(from: mail, event: event) - } - - saveToDisk(json: dictToJSON(fields: event)) sendCheck() } - + static func log(sendViewOpen mail: EphemeralMail?) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.sendViewOpen.rawValue - if let mail = mail { - event = extract(from: mail, event: event) - } - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(sendViewClose mail: EphemeralMail?) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.sendViewClose.rawValue - if let mail = mail { - event = extract(from: mail, event: event) - } - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(createDraft to: [Mail_Address?], cc: [Mail_Address?], bcc: [Mail_Address?], subject: String, bodyLength: Int, isEncrypted: Bool, isSigned: Bool, myKeyID: String) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.createDraft.rawValue - - - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(sent from: Mail_Address, to: [Mail_Address], cc: [Mail_Address], bcc: [Mail_Address], subject: String, bodyLength: Int, isEncrypted: Bool, decryptedBodyLength: Int, decryptedWithOldPrivateKey: Bool = false, isSigned: Bool, isCorrectlySigned: Bool = true, signingKeyID: String, myKeyID: String, secureAddresses: [Mail_Address] = [], encryptedForKeyIDs: [String] = [], inviteMailContent: String?, invitationMail: Bool) { - + if !logging { return } - - var event = plainLogDict() - - event["type"] = LoggingEventType.mailSent.rawValue - event["from"] = Logger.resolve(mailAddress: from) - event["to"] = Logger.resolve(mailAddresses: to) - event["cc"] = Logger.resolve(mailAddresses: cc) - event["bcc"] = Logger.resolve(mailAddresses: bcc) - event["communicationState"] = Logger.communicationState(subject: subject) - event["specialMail"] = Logger.specialMail(subject: subject) - event["bodyLength"] = bodyLength - event["isEncrypted"] = isEncrypted - event["decryptedBodyLength"] = decryptedBodyLength - event["decryptedWithOldPrivateKey"] = decryptedWithOldPrivateKey - event["isSigned"] = isSigned - event["isCorrectlySigned"] = isCorrectlySigned - event["signingKeyID"] = Logger.resolve(keyID: signingKeyID) - event["myKeyID"] = Logger.resolve(keyID: myKeyID) - event["secureAddresses"] = Logger.resolve(mailAddresses: secureAddresses) //means the addresses, which received a secure mail - event["encryptedForKeyIDs"] = Logger.resolve(keyIDs: encryptedForKeyIDs) - if let content = inviteMailContent { - event["inviteMailContent"] = content + var encState = EncryptionState.NoEncryption + if isEncrypted { + encState = EncryptionState.ValidedEncryptedWithCurrentKey } - - saveToDisk(json: dictToJSON(fields: event)) - if invitationMail { - sendLog() - } - else { - sendCheck() + var sigState = SignatureState.NoSignature + if isSigned { + sigState = SignatureState.ValidSignature } - - + LogData.addMail(enc: encState , sig: sigState, received: false) + sendCheck() } - + static func log(readViewOpen mail: PersistentMail, message: String, draft: Bool = false) { if !logging { return } - - var event = plainLogDict() - - event["type"] = LoggingEventType.readViewOpen.rawValue - event = extract(from: mail, event: event) - event["messagePresented"] = message - event["draft"] = draft - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(readViewClose message: String, draft: Bool = false) { if !logging { return } - - var event = plainLogDict() - - event["type"] = LoggingEventType.readViewClose.rawValue - event["messagePresented"] = message - event["draft"] = draft - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(received mail: PersistentMail) { if !logging { return } - - var event = plainLogDict() - - event["type"] = LoggingEventType.mailReceived.rawValue - event = extract(from: mail, event: event) - - saveToDisk(json: dictToJSON(fields: event)) + LogData.addMail(mail: mail, received: true) sendCheck() } - + static func log(bitcoinMail gotIt: Bool) { if !logging { return } - - var event = plainLogDict() - - event["type"] = LoggingEventType.gotBitcoinMail.rawValue - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - - + + static func log(delete mail: PersistentMail, toTrash: Bool) { if !logging { return } - - var event = plainLogDict() - if toTrash { - event["type"] = LoggingEventType.mailDeletedToTrash.rawValue - } else { - event["type"] = LoggingEventType.mailDeletedPersistent.rawValue - } - // event = extract(from: mail, event: event) - event["operation"] = "DeleteMail" - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(archive mail: PersistentMail) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.mailArchived.rawValue - event = extract(from: mail, event: event) - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(open indicatorButton: String, mail: PersistentMail?) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.indicatorButtonOpen.rawValue - event["indicatorButton"] = indicatorButton - if let mail = mail { - event["view"] = "readView" - event = extract(from: mail, event: event) - } else { - event["view"] = "sendView" - } - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - - static func log(close indicatorButton: String, mail: PersistentMail?, action: String) { + + static func log(close indicatorButton: LogData.IndicatorButton, mail: PersistentMail?, action: LogData.UserInteractionPopUp, duration: Double) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.indicatorButtonClose.rawValue - event["indicatorButton"] = indicatorButton - if let mail = mail { - event["view"] = "readView" - event = extract(from: mail, event: event) - } else { - event["view"] = "sendView" - } - event["action"] = action - - saveToDisk(json: dictToJSON(fields: event)) + LogData.clickOnIndicator(type: indicatorButton, userReaction: action, duration: duration) sendCheck() } - + static func log(showBroken mail: PersistentMail?) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.showBrokenMail.rawValue - event["view"] = "readView" - if let mail = mail { - event = extract(from: mail, event: event) - } - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - + static func log(reactTo mail: PersistentMail?) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.reactButtonTapped.rawValue - if let mail = mail { - event = extract(from: mail, event: event) - } - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() + } - - static func log(discover publicKeyID: String, mailAddress: Mail_Address, importChannel: String, knownPrivateKey: Bool, knownBefore: Bool) { //add reference to mail here? + + static func log(discover publicKeyID: String, mailAddress: Mail_Address, importChannel: LogData.TransferType, knownPrivateKey: Bool, knownBefore: Bool) { if !logging { return } - - var event = plainLogDict() - if !knownBefore { - event["type"] = LoggingEventType.pubKeyDiscoveryNewKey.rawValue - } else { - event["type"] = LoggingEventType.pubKeyDiscoveryKnownKey.rawValue - } - event["keyID"] = Logger.resolve(keyID: publicKeyID) - event["mailAddress"] = Logger.resolve(mail_address: mailAddress) - event["knownPrivateKey"] = knownPrivateKey //Do we have a private key for it? - event["importChannel"] = importChannel - - saveToDisk(json: dictToJSON(fields: event)) + LogData.importKey(keyType: .publicKey, transferType: importChannel, known: knownBefore) sendCheck() } - + static func log(backgroundFetch newMails: UInt32, duration: Double) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.backgroundFetch.rawValue - event["newMails"] = Int(newMails) - event["duration"] = duration - - saveToDisk(json: dictToJSON(fields: event)) } static func log(verify keyID: String, open: Bool, success: Bool? = nil) { if !logging { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.pubKeyVerification.rawValue - event["keyID"] = Logger.resolve(keyID: keyID) - var stateString = "open" - if !open { - stateString = "close" - } - event["state"] = stateString - if let success = success { - event["success"] = success - } - - saveToDisk(json: dictToJSON(fields: event)) + LogData.newVerification(successful: success) sendCheck() } - - /** - - parameters: - - nrOfTrays: Number of search results - - category: 0 is sender, 1 is subject, 2 is message, 3 is everything - - opened: "mail" User opened a message, - "mailList" User opened the mail list, - "contact" User opened contact view, - "searchedInMailList" User searched in the ListView. - - keyRecordMailList: Array of MailAddresses that identify the KeyRecord in which the user searched. Nil when not in MailListView. - */ + static func log(search nrOfTrays: Int, category: Int, opened: String, keyRecordMailList: [MailAddress]? = nil) { guard logging else { return } - - var event = plainLogDict() - event["type"] = LoggingEventType.search.rawValue - event["category"] = category - event["nrOfTrays"] = nrOfTrays - event["opened"] = opened - event["keyRecordMailList"] = resolve(mailAddresses: keyRecordMailList ?? []) - - saveToDisk(json: dictToJSON(fields: event)) - sendCheck() } - static fileprivate func extract(from mail: PersistentMail, event: [String: Any]) -> [String: Any] { - var event = event - event["from"] = Logger.resolve(mailAddress: mail.from) - event["to"] = Logger.resolve(mailAddresses: mail.to) - event["cc"] = Logger.resolve(mailAddresses: mail.cc ?? NSSet()) - event["bcc"] = Logger.resolve(mailAddresses: mail.bcc ?? NSSet()) - event["communicationState"] = Logger.communicationState(subject: mail.subject ?? "") - event["specialMail"] = Logger.specialMail(subject: mail.subject ?? "") - event["timeInHeader"] = mail.date.description - event["bodyLength"] = (mail.body)?.count - event["isEncrypted"] = mail.isEncrypted - event["decryptedBodyLength"] = (mail.decryptedBody ?? "").count - event["decryptedWithOldPrivateKey"] = mail.decryptedWithOldPrivateKey - event["isSigned"] = mail.isSigned - event["isCorrectlySigned"] = mail.isCorrectlySigned - event["x-Mailer"] = mail.xMailer - //TODO: - //event["signingKeyID"] = Logger.resolve(keyID: signingKeyID) - //event["myKeyID"] = Logger.resolve(keyID: myKeyID) - - - - //event["secureAddresses"] = secureAddresses //could mean the addresses, in this mail we have a key for - //event["encryptedForKeyIDs"] = Logger.resolve(keyIDs: encryptedForKeyIDs) - - event["trouble"] = mail.trouble - event["folder"] = Logger.resolve(folder: mail.folder) - - return event + static private func sendLog(logMailAddress: String = logReceiver) { + let jsonEncoder = JSONEncoder() + if let data = try? jsonEncoder.encode(Maildata()), let text = String(data: data, encoding: .utf8), text.count > 0 { + AppDelegate.getAppDelegate().mailHandler.send([logMailAddress], ccEntrys: [], bccEntrys: [], subject: "[Enzevalos] Log", message: text, callback: sendCallback, loggingMail: true) + } + LogInUserDefaults.handler.reset() } + + static func sendCallback(error: Error?) { + guard error == nil else { + return + } + + let tmpNextDeadline = Date(timeIntervalSinceNow: TimeInterval(loggingInterval)) + nextDeadline = tmpNextDeadline + UserManager.storeUserValue(nextDeadline as AnyObject?, attribute: Attribute.nextDeadline) + } + static func log(warning: LogData.WarningType) { + LogData.warning(type: warning) + } + + static func log(reactWarning: LogData.WarningType, reaction: LogData.UserWarningReaction) { + LogData.warning(type: reactWarning, reaction: reaction) + } +} - static fileprivate func extract(from mail: EphemeralMail, event: [String: Any]) -> [String: Any] { - var event = event - event["to"] = Logger.resolve(mailAddresses: mail.to) - event["cc"] = Logger.resolve(mailAddresses: mail.cc ?? NSSet()) - event["bcc"] = Logger.resolve(mailAddresses: mail.bcc ?? NSSet()) - event["communicationState"] = Logger.communicationState(subject: mail.subject ?? "") - event["specialMail"] = Logger.specialMail(subject: mail.subject ?? "") - event["bodyLength"] = (mail.body)?.count - //TODO: - //event["signingKeyID"] = Logger.resolve(keyID: signingKeyID) - //event["myKeyID"] = Logger.resolve(keyID: myKeyID) - - - - //event["secureAddresses"] = secureAddresses //could mean the addresses, in this mail we have a key for - //event["encryptedForKeyIDs"] = Logger.resolve(keyIDs: encryptedForKeyIDs) +/* + Logged data according privacy policy (see: https://letterbox-app.org/privacy.html) + */ - return event +public class LogInUserDefaults { + static let handler = LogInUserDefaults() + + private let domain = "letterbox.logging" + private let logger: UserDefaults + private let startDateName = "startDate" + private let prefix = "letterbox" + + init() { + if let newDefault = UserDefaults.init(suiteName: domain) { + logger = newDefault + } + else { + logger = UserDefaults.standard + } + if logger.data(forKey: startDateName) == nil { + logger.set(Date(), forKey: "\(prefix).\(startDateName)") + } } - - static func communicationState(subject: String) -> String { - if subject == "" { - return "" + + func incrementEventCounter(eventName: String) { + let oldValue = logger.integer(forKey: "\(prefix).\(eventName)") + let counter = oldValue + 1 + logger.set(counter, forKey: "\(prefix).\(eventName)") + + } + + func reset() { + for (key, value) in logger.dictionaryRepresentation() { + if let _ = value as? Int { + logger.set(0, forKey: key) + } } - var oldSubject = subject - var newSubject = "" - var hasPrefix = true - - while hasPrefix { - if oldSubject.hasPrefix("Re: ") || oldSubject.hasPrefix("RE: ") || oldSubject.hasPrefix("re: ") || oldSubject.hasPrefix("AW: ") || oldSubject.hasPrefix("Aw: ") || oldSubject.hasPrefix("aw: ") { - newSubject += "Re: " - oldSubject = String(oldSubject[oldSubject.index(oldSubject.startIndex, offsetBy: 4)...]) //damn swift3! - } else if oldSubject.hasPrefix("Fwd: ") || oldSubject.hasPrefix("FWD: ") || oldSubject.hasPrefix("fwd: ") { - newSubject += "Fwd: " - oldSubject = String(oldSubject[oldSubject.index(oldSubject.startIndex, offsetBy: 5)...]) - } else if oldSubject.hasPrefix("WG: ") || oldSubject.hasPrefix("Wg: ") || oldSubject.hasPrefix("wg: ") { - newSubject += "Fwd: " - oldSubject = String(oldSubject[oldSubject.index(oldSubject.startIndex, offsetBy: 4)...]) - } else { - hasPrefix = false + logger.set(Date(), forKey: "\(prefix).\(startDateName)") + } + + func calculateDuration(eventName: String, duration: Double) { + // Use online algo: See https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance + let nameMean = "|mean" + let nameSqD = "|sqD" // squared distance from the mean + // load values + let n = logger.integer(forKey: "\(prefix).\(eventName)") // we assume that event was logged before! + var mean = logger.double(forKey: "\(prefix).\(eventName+nameMean)") + var sqD = logger.double(forKey: "\(prefix).\(eventName+nameSqD)") + + // See: + // Welford, B. P. (1962). "Note on a method for calculating corrected sums of squares and products". or + // Donald E. Knuth (1998). The Art of Computer Programming, volume 2: Seminumerical Algorithms, 3rd edn., p. 232 + let delta = duration - mean + mean = mean + (delta / Double(n)) + let delta2 = duration - mean + sqD = sqD + delta * delta2 + + // update values + logger.set(sqD, forKey: "\(prefix).\(eventName+nameSqD)") + logger.set(mean, forKey: "\(prefix).\(eventName+nameMean)") + } + + var loggedEvents: [String: Int] { + get { + var events = [String: Int]() + for (key, value) in logger.dictionaryRepresentation() { + if key.starts(with: prefix), let v = value as? Int { + events[key] = v + } } + return events } - - return newSubject } - - static func specialMail(subject: String) -> String { - if subject.contains(NSLocalizedString("inviteSubject", comment: "subject of invitation email")) { - return "invitation" + + var loggedDuration: [String: Double] { + var events = [String: Double]() + for (key, value) in logger.dictionaryRepresentation() { + if key.starts(with: prefix), let v = value as? Double { + events[key] = v + } } - return "" + return events } +} - //takes backendFolderPath - static func resolve(folder: Folder) -> String { - let folderPath = folder.path - if folderPath == UserManager.backendSentFolderPath { - return "sent" - } - if folderPath == UserManager.backendDraftFolderPath { - return "draft" +public class LogData { + + static func newSimpleEvent(event: SimpleEvents) { + LogInUserDefaults.handler.incrementEventCounter(eventName: event.name) + } + static func addMail(mail: PersistentMail, received: Bool) { + let enc = mail.encState + let sig = mail.sigState + addMail(enc: enc, sig: sig, received: received) + } + + static func addMail(enc: EncryptionState, sig: SignatureState, received: Bool) { + let key = LogData.makeMailKey(enc: enc, sig: sig, received: received) + LogInUserDefaults.handler.incrementEventCounter(eventName: key) + } + + private static func makeMailKey(enc: EncryptionState, sig: SignatureState, received: Bool) -> String { + var key = "\(enc.name)&\(sig.name)" + if received { + key = "in|" + key } - if folderPath == UserManager.backendInboxFolderPath { - return "inbox" + else { + key = "out|"+key } - if folderPath == UserManager.backendTrashFolderPath { - return "trash" + return key + } + + static func newVerification(successful: Bool?) { + var event = "verification" + if let successful = successful { + if successful { + event = "successful|"+event + } + else { + event = "failed|"+event + } } - if folderPath == UserManager.backendArchiveFolderPath { - return "archive" + else { + event = "unknown|"+event } - return folder.pseudonym + + LogInUserDefaults.handler.incrementEventCounter(eventName: event) } - - //get an pseudonym for a mailAddress - static func resolve(mailAddress: MailAddress) -> String { - if let addr = mailAddress as? Mail_Address { - return resolve(mail_address: addr) - } else if mailAddress is CNMailAddressExtension { - return "CNMailAddress" + + static func importKey(keyType: KeyType, transferType: TransferType, known: Bool) { + var new = "strange" + if known { + new = "known" } - return "unknownMailAddressType" + LogInUserDefaults.handler.incrementEventCounter(eventName: "importKey|\(keyType.name)&\(transferType.name)&\(new)") } - - static func resolve(mail_address: Mail_Address) -> String { - if mail_address.isUser { - return "self" - } - return mail_address.pseudonym + + static func exportKey(keyType: KeyType, transferType: TransferType) { + LogInUserDefaults.handler.incrementEventCounter(eventName: "exportKey|\(keyType.name)&\(transferType.name)") } - static func resolve(mailAddresses: NSSet) -> [String] { - var result: [String] = [] - for addr in mailAddresses { - if let addr = addr as? Mail_Address { - result.append(resolve(mail_address: addr)) - } else if addr is CNMailAddressExtension { - result.append("CNMailAddress") - } else { - result.append("unknownMailAddressType") + + static func warning(type: WarningType, reaction: UserWarningReaction) { + let name = "Warning|\(type.name)&\(reaction.name)" + LogInUserDefaults.handler.incrementEventCounter(eventName: name) + } + + static func warning(type: WarningType) { + let name = "Warning|\(type.name)" + LogInUserDefaults.handler.incrementEventCounter(eventName: name) + } + + static func clickOnIndicator(type: IndicatorButton, userReaction: UserInteractionPopUp, duration: Double) { + let name = "clickIndicator:\(type.name)&\(userReaction)" + LogInUserDefaults.handler.incrementEventCounter(eventName: name) + LogInUserDefaults.handler.calculateDuration(eventName: name, duration: duration) + } + + static func viewOnboardingSlide(type: IntroState, duration: Double) { + LogInUserDefaults.handler.incrementEventCounter(eventName: type.name) + LogInUserDefaults.handler.calculateDuration(eventName: type.name, duration: duration) + } + + enum SimpleEvents { + case newSession, keyDetailsView, invationMail, loginAttempt + + var name: String { + get { + switch self { + case .newSession: + return "newSession" + case .keyDetailsView: + return "keyDetailsView" + case .invationMail: + return "inviation" + case .loginAttempt: + return "loginAttempt" + } } } - return result } - - static func resolve(mailAddresses: [Mail_Address]) -> [String] { - var result: [String] = [] - for addr in mailAddresses { - result.append(resolve(mail_address: addr)) + + enum KeyType { + case publicKey, secretKey + + var name: String { + switch self { + case .publicKey: + return "PublicKey" + case .secretKey: + return "SecretKey" + } } - return result } - - static func resolve(mailAddresses: [MailAddress]) -> [String] { - var result: [String] = [] - for addr in mailAddresses { - if let addr = addr as? Mail_Address { - result.append(resolve(mail_address: addr)) - } else if addr is CNMailAddressExtension { - result.append("CNMailAddress") - } else { - result.append("unknownMailAddressType") + + enum TransferType { + case autocrypt, mail, iTunes, qr, generated + + var name: String { + switch self { + case .autocrypt: + return "autocrypt" + case .mail: + return "mail" + case .iTunes: + return "iTunes" + case .qr: + return "QR-Code" + case .generated: + return "generated" } } - return result } - - //get an pseudonym for a keyID - static func resolve(keyID: String) -> String { - if let key = DataHandler.handler.findKey(keyID: keyID) { - return key.pseudonym + + enum WarningType { + case newKey, notConfidential, error, none, unableToDecrypt, unableToDecryptTravel, deletedWhileTravel + var name: String { + get { + switch self { + case .newKey: + return "newKey" + case .notConfidential: + return "notConfidential" + case .error: + return "error" + case .none: + return "none" + case .unableToDecrypt: + return "unableToDecrypt" + case .unableToDecryptTravel: + return "unableToDecryptTravel" + case .deletedWhileTravel: + return "deletedWhileTravel" + } + } } - return "noKeyID" - } - - static func resolve(key: PersistentKey) -> String { - return key.pseudonym } - - static func resolve(keyIDs: [String]) -> [String] { - var result: [String] = [] - for id in keyIDs { - result.append(resolve(keyID: id)) + + enum UserWarningReaction { + case ignore, requestConfirmation, clickButton + + var name: String { + get { + switch self { + case .ignore: + return "ignore" + case .requestConfirmation: + return "requestConfirmation" + case .clickButton: + return "clickButton" + } + } } - return result } - - static func saveToDisk(json: String, fileName: String = defaultFileName) { - - if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - - let fileURL = dir.appendingPathComponent(fileName) - - if FileManager.default.fileExists(atPath: fileURL.path) { - // append - do { - let fileHandle = try FileHandle(forUpdating: fileURL) - - fileHandle.seekToEndOfFile() - if let encoded = "\n\(json),".data(using: .utf8) { - fileHandle.write(encoded) - } - fileHandle.closeFile() - } - catch { - print("Error while appending to logfile: \(error.localizedDescription)") - } - } else { - // write new - do { - try json.write(to: fileURL, atomically: false, encoding: .utf8) - } - catch { - print("Error while writing logfile: \(error.localizedDescription)") + + enum IndicatorButton { + case confidential, signedOnly, encryptedOnly, notConfidential, unableToDecrypt, error + + var name: String { + get { + switch self { + case .confidential: + return "confidential" + case .signedOnly: + return "signedOnly" + case . encryptedOnly: + return "encryptedOnly" + case .notConfidential: + return "NotConfidential" + case .unableToDecrypt: + return "unableToDecrypt" + case .error: + return "error" } } - } } - - static func sendLog(fileName: String = defaultFileName, logMailAddress: String = logReceiver) { - - if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - - let fileURL = dir.appendingPathComponent(fileName) - - // reading - do { - let currentContent = try String(contentsOf: fileURL, encoding: .utf8) - if !currentContent.isEmpty { - AppDelegate.getAppDelegate().mailHandler.send([logMailAddress], ccEntrys: [], bccEntrys: [], subject: "[Enzevalos] Log", message: "{\"studyID\":\"" + studyID + "\",\"data\":" + "[" + currentContent.dropLast() + "\n]" + "}", callback: sendCallback, loggingMail: true) + + enum UserInteractionPopUp { + case close, inviteOther, disableConf, enableConf, exportKey, moreInfo, travelFollowUp, EnableEnforceConf, DisableEnforceConf + + var name: String { + get { + switch self { + case .close: + return "close" + case .disableConf: + return "disableConf" + case .enableConf: + return "enableConfAgain" + case .inviteOther: + return "invite" + case .exportKey: + return "exportKey" + case .moreInfo: + return "moreInformation" + case .travelFollowUp: + return "travelFollowUp" + case .EnableEnforceConf: + return "enforceConf" + case .DisableEnforceConf: + return "turnEnforceConfOff" + } } - catch { - print("Error while reading logfile: \(error.localizedDescription)") - } } } +} - static func sendCallback(error: Error?) { - guard error == nil else { - print(error!) - return +/* + Outgoing data: + reads stored values and + calculates other data + send data + */ +public class Maildata: Encodable { + let studyID = StudySettings.studyID + var studySetting: String { + get { + let symbol = StudySettings.securityIndicator.rawValue + return "studyparameter|\(symbol)" } - - clearLog() - let tmpNextDeadline = Date(timeIntervalSinceNow: TimeInterval(loggingInterval)) - nextDeadline = tmpNextDeadline - UserManager.storeUserValue(nextDeadline as AnyObject?, attribute: Attribute.nextDeadline) } + var AddresseswithoutKeys: Int = 0 + var addressesWithKeys: [Int: Int] = [:] + let storedDurations = LogInUserDefaults.handler.loggedDuration - static func clearLog(fileName: String = defaultFileName) { - if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - - let fileURL = dir.appendingPathComponent(fileName) - - do { - try String().write(to: fileURL, atomically: false, encoding: .utf8) - } - catch { - print("Error while clearing logfile: \(error.localizedDescription)") - } - } + + + init() { + self.collectPublicKeyInfos() + } + + + private func collectPublicKeyInfos() { + let handler = DataHandler.handler + let numOfNoKeyAddr = handler.findMailAddress(withKey: false).count + let numOfKeyAddr = handler.findMailAddress(withKey: true) + var numOfKeysPerAddr = [Int: Int] () + for addr in numOfKeyAddr { + let counterKeys = addr.publicKeys.count + let before = numOfKeysPerAddr[counterKeys] ?? 0 + numOfKeysPerAddr[counterKeys] = before + 1 + } + AddresseswithoutKeys = numOfNoKeyAddr + addressesWithKeys = numOfKeysPerAddr } } diff --git a/enzevalos_iphone/LoggerDetail.swift b/enzevalos_iphone/LoggerDetail.swift new file mode 100644 index 0000000000000000000000000000000000000000..82d396827bb34dd5dcd1697c069ce36fe94c1f67 --- /dev/null +++ b/enzevalos_iphone/LoggerDetail.swift @@ -0,0 +1,948 @@ +// +// Logger.swift +// enzevalos_iphone +// +// Created by jakobsbode on 25.10.17. +// Copyright © 2018 fu-berlin. +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// + +import Foundation + +class LoggerDetail { + + static var logging = StudySettings.studyMode + + static let queue = DispatchQueue(label: "logging", qos: .background) + + static let defaultFileName = "log.json" + static let loggingInterval = 21600 //60*60*6 seconds = 6 hours + static let resendInterval = 5 * 60 + static let logReceiver = LOGGING_MAIL_ADR + + static var nextDeadline = (UserManager.loadUserValue(Attribute.nextDeadline) as? Date) ?? Date() + + static var studyID = StudySettings.studyID //identifies the participant in the study + + + + static fileprivate func sendCheck() { + if nextDeadline <= Date() && AppDelegate.getAppDelegate().currentReachabilityStatus != .notReachable && UserManager.loadUserValue(Attribute.accountname) != nil && UserDefaults.standard.bool(forKey: "launchedBefore"){ + //Do not send duplicate mails + let tmpNextDeadline = Date(timeIntervalSinceNow: TimeInterval(resendInterval)) + nextDeadline = tmpNextDeadline + UserManager.storeUserValue(nextDeadline as AnyObject?, attribute: Attribute.nextDeadline) + sendLog() + } + } + + static fileprivate func plainLogDict() -> [String: Any] { + var fields: [String: Any] = [:] + let now = Date() + fields["timestamp"] = now.description + fields["lang"] = Locale.current.languageCode + return fields + } + + static fileprivate func dictToJSON(fields: [String: Any]) -> String { + do { + let jsonData = try JSONSerialization.data(withJSONObject: fields) + if let json = String(data: jsonData, encoding: String.Encoding.utf8) { + return json + } + return "" + } catch { + return "{\"error\":\"json conversion failed\"}" + } + } + + static func log(setupStudy studypara: [StudyParameterProtocol.Type], alreadyRegistered: Bool) { + if !logging { + return + } + var event = plainLogDict() + event["type"] = LoggingEventType.setupStudy.rawValue + + //event["language"] = + + for para in studypara{ + event[para.name] = para.load().value + } + event["alreadyRegistered"] = alreadyRegistered + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(startApp onboarding: Bool) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.appStart.rawValue + event["onboarding"] = onboarding + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(terminateApp: Void) { + if !logging { + return + } + var event = plainLogDict() + event["type"] = LoggingEventType.appTerminate.rawValue + saveToDisk(json: dictToJSON(fields: event)) + } + + static func log(background goto: Bool) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.appBackground.rawValue + event["going to"] = goto //true -> goto background; false -> comming from background + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(onboardingState onboardingSection: String) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.onboardingState.rawValue + event["onboardingSection"] = onboardingSection + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(onboardingPageTransition from: Int, to: Int, onboardingSection: String) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.onboardingPageTransition.rawValue + event["from"] = from + event["to"] = to + event["onboardingSection"] = onboardingSection + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(contactViewOpen keyRecord: KeyRecord?, otherRecords: [KeyRecord]?, isUser: Bool) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.contactViewOpen.rawValue + + if let keyRecord = keyRecord { + if let keyID = keyRecord.keyID { + event["keyID"] = resolve(keyID: keyID) + } else { + event["keyID"] = "nil" + } + event["mailaddresses"] = resolve(mailAddresses: keyRecord.addresses) + } + event["isUser"] = isUser + if isUser { + let (contact, mail) = GamificationData.sharedInstance.getSecureProgress() + event["gamificationContact"] = contact + event["gamificationMail"] = mail + } + event["numberOfOtherRecords"] = (otherRecords ?? []).count + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(badgeCaseViewOpen badges: [Badges]) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.badgeCaseViewOpen.rawValue + + var achievedBadges: [String] = [] + var missingBadges: [String] = [] + + for badge in badges { + if badge.isAchieved() { + achievedBadges.append(badge.displayName) + } else { + missingBadges.append(badge.displayName) + } + } + + event["achievedBadges"] = achievedBadges + event["missingBadges"] = missingBadges + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(badgeCaseViewClose badges: [Badges]) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.badgeCaseViewClose.rawValue + + var achievedBadges: [String] = [] + var missingBadges: [String] = [] + + for badge in badges { + if badge.isAchieved() { + achievedBadges.append(badge.displayName) + } else { + missingBadges.append(badge.displayName) + } + } + + event["achievedBadges"] = achievedBadges + event["missingBadges"] = missingBadges + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(contactViewClose keyRecord: KeyRecord?, otherRecords: [KeyRecord]?, isUser: Bool) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.contactViewClose.rawValue + + if let keyRecord = keyRecord { + if let keyID = keyRecord.keyID { + event["keyID"] = resolve(keyID: keyID) + } else { + event["keyID"] = "nil" + } + event["mailaddresses"] = resolve(mailAddresses: keyRecord.addresses) + } + event["isUser"] = isUser + if isUser { + let (contact, mail) = GamificationData.sharedInstance.getSecureProgress() + event["gamificationContact"] = contact + event["gamificationMail"] = mail + } + event["numberOfOtherRecords"] = (otherRecords ?? []).count + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(keyViewOpen keyID: String) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.keyViewOpen.rawValue + event["keyID"] = resolve(keyID: keyID) + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(keyViewClose keyID: String, secondsOpened: Int) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.keyViewClose.rawValue + event["keyID"] = resolve(keyID: keyID) + event["opened for seconds"] = secondsOpened + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(exportKeyViewOpen view: Int) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.exportKeyViewOpen.rawValue + event["view"] = view + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(exportKeyViewClose view: Int) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.exportKeyViewClose.rawValue + event["view"] = view + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(exportKeyViewButton send: Bool) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.exportKeyViewClose.rawValue + if send { + event["case"] = "sendMail" + } else { + event["case"] = "deletePasscode" + } + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(importPrivateKeyPopupOpen mail: PersistentMail?) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.importPrivateKeyPopupOpen.rawValue + if let mail = mail { + event = extract(from: mail, event: event) + } + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(importPrivateKeyPopupClose mail: PersistentMail?, doImport: Bool) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.importPrivateKeyPopupClose.rawValue + event["doImport"] = doImport + if let mail = mail { + event = extract(from: mail, event: event) + } + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(importPrivateKey mail: PersistentMail?, success: Bool) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.importPrivateKey.rawValue + event["success"] = success + if let mail = mail { + event = extract(from: mail, event: event) + } + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(sendViewOpen mail: EphemeralMail?) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.sendViewOpen.rawValue + if let mail = mail { + event = extract(from: mail, event: event) + } + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(sendViewClose mail: EphemeralMail?) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.sendViewClose.rawValue + if let mail = mail { + event = extract(from: mail, event: event) + } + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(createDraft to: [Mail_Address?], cc: [Mail_Address?], bcc: [Mail_Address?], subject: String, bodyLength: Int, isEncrypted: Bool, isSigned: Bool, myKeyID: String) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.createDraft.rawValue + + + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(sent from: Mail_Address, to: [Mail_Address], cc: [Mail_Address], bcc: [Mail_Address], subject: String, bodyLength: Int, isEncrypted: Bool, decryptedBodyLength: Int, decryptedWithOldPrivateKey: Bool = false, isSigned: Bool, isCorrectlySigned: Bool = true, signingKeyID: String, myKeyID: String, secureAddresses: [Mail_Address] = [], encryptedForKeyIDs: [String] = [], inviteMailContent: String?, invitationMail: Bool) { + + if !logging { + return + } + + var event = plainLogDict() + + event["type"] = LoggingEventType.mailSent.rawValue + event["from"] = LoggerDetail.resolve(mailAddress: from) + event["to"] = LoggerDetail.resolve(mailAddresses: to) + event["cc"] = LoggerDetail.resolve(mailAddresses: cc) + event["bcc"] = LoggerDetail.resolve(mailAddresses: bcc) + event["communicationState"] = LoggerDetail.communicationState(subject: subject) + event["specialMail"] = LoggerDetail.specialMail(subject: subject) + event["bodyLength"] = bodyLength + event["isEncrypted"] = isEncrypted + event["decryptedBodyLength"] = decryptedBodyLength + event["decryptedWithOldPrivateKey"] = decryptedWithOldPrivateKey + event["isSigned"] = isSigned + event["isCorrectlySigned"] = isCorrectlySigned + event["signingKeyID"] = LoggerDetail.resolve(keyID: signingKeyID) + event["myKeyID"] = LoggerDetail.resolve(keyID: myKeyID) + event["secureAddresses"] = LoggerDetail.resolve(mailAddresses: secureAddresses) //means the addresses, which received a secure mail + event["encryptedForKeyIDs"] = LoggerDetail.resolve(keyIDs: encryptedForKeyIDs) + if let content = inviteMailContent { + event["inviteMailContent"] = content + } + + saveToDisk(json: dictToJSON(fields: event)) + if invitationMail { + sendLog() + } + else { + sendCheck() + } + + + } + + static func log(readViewOpen mail: PersistentMail, message: String, draft: Bool = false) { + if !logging { + return + } + + var event = plainLogDict() + + event["type"] = LoggingEventType.readViewOpen.rawValue + event = extract(from: mail, event: event) + event["messagePresented"] = message + event["draft"] = draft + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(readViewClose message: String, draft: Bool = false) { + if !logging { + return + } + + var event = plainLogDict() + + event["type"] = LoggingEventType.readViewClose.rawValue + event["messagePresented"] = message + event["draft"] = draft + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(received mail: PersistentMail) { + if !logging { + return + } + + var event = plainLogDict() + + event["type"] = LoggingEventType.mailReceived.rawValue + event = extract(from: mail, event: event) + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(bitcoinMail gotIt: Bool) { + if !logging { + return + } + + var event = plainLogDict() + + event["type"] = LoggingEventType.gotBitcoinMail.rawValue + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + + static func log(delete mail: PersistentMail, toTrash: Bool) { + if !logging { + return + } + + var event = plainLogDict() + if toTrash { + event["type"] = LoggingEventType.mailDeletedToTrash.rawValue + } else { + event["type"] = LoggingEventType.mailDeletedPersistent.rawValue + } + // event = extract(from: mail, event: event) + event["operation"] = "DeleteMail" + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(archive mail: PersistentMail) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.mailArchived.rawValue + event = extract(from: mail, event: event) + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(open indicatorButton: String, mail: PersistentMail?) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.indicatorButtonOpen.rawValue + event["indicatorButton"] = indicatorButton + if let mail = mail { + event["view"] = "readView" + event = extract(from: mail, event: event) + } else { + event["view"] = "sendView" + } + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(close indicatorButton: String, mail: PersistentMail?, action: String) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.indicatorButtonClose.rawValue + event["indicatorButton"] = indicatorButton + if let mail = mail { + event["view"] = "readView" + event = extract(from: mail, event: event) + } else { + event["view"] = "sendView" + } + event["action"] = action + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(showBroken mail: PersistentMail?) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.showBrokenMail.rawValue + event["view"] = "readView" + if let mail = mail { + event = extract(from: mail, event: event) + } + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(reactTo mail: PersistentMail?) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.reactButtonTapped.rawValue + if let mail = mail { + event = extract(from: mail, event: event) + } + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(discover publicKeyID: String, mailAddress: Mail_Address, importChannel: String, knownPrivateKey: Bool, knownBefore: Bool) { //add reference to mail here? + if !logging { + return + } + + var event = plainLogDict() + if !knownBefore { + event["type"] = LoggingEventType.pubKeyDiscoveryNewKey.rawValue + } else { + event["type"] = LoggingEventType.pubKeyDiscoveryKnownKey.rawValue + } + event["keyID"] = LoggerDetail.resolve(keyID: publicKeyID) + event["mailAddress"] = LoggerDetail.resolve(mail_address: mailAddress) + event["knownPrivateKey"] = knownPrivateKey //Do we have a private key for it? + event["importChannel"] = importChannel + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static func log(backgroundFetch newMails: UInt32, duration: Double) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.backgroundFetch.rawValue + event["newMails"] = Int(newMails) + event["duration"] = duration + + saveToDisk(json: dictToJSON(fields: event)) + } + + static func log(verify keyID: String, open: Bool, success: Bool? = nil) { + if !logging { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.pubKeyVerification.rawValue + event["keyID"] = LoggerDetail.resolve(keyID: keyID) + var stateString = "open" + if !open { + stateString = "close" + } + event["state"] = stateString + if let success = success { + event["success"] = success + } + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + /** + - parameters: + - nrOfTrays: Number of search results + - category: 0 is sender, 1 is subject, 2 is message, 3 is everything + - opened: "mail" User opened a message, + "mailList" User opened the mail list, + "contact" User opened contact view, + "searchedInMailList" User searched in the ListView. + - keyRecordMailList: Array of MailAddresses that identify the KeyRecord in which the user searched. Nil when not in MailListView. + */ + static func log(search nrOfTrays: Int, category: Int, opened: String, keyRecordMailList: [MailAddress]? = nil) { + guard logging else { + return + } + + var event = plainLogDict() + event["type"] = LoggingEventType.search.rawValue + event["category"] = category + event["nrOfTrays"] = nrOfTrays + event["opened"] = opened + event["keyRecordMailList"] = resolve(mailAddresses: keyRecordMailList ?? []) + + saveToDisk(json: dictToJSON(fields: event)) + sendCheck() + } + + static fileprivate func extract(from mail: PersistentMail, event: [String: Any]) -> [String: Any] { + var event = event + event["from"] = LoggerDetail.resolve(mailAddress: mail.from) + event["to"] = LoggerDetail.resolve(mailAddresses: mail.to) + event["cc"] = LoggerDetail.resolve(mailAddresses: mail.cc ?? NSSet()) + event["bcc"] = LoggerDetail.resolve(mailAddresses: mail.bcc ?? NSSet()) + event["communicationState"] = LoggerDetail.communicationState(subject: mail.subject ?? "") + event["specialMail"] = LoggerDetail.specialMail(subject: mail.subject ?? "") + event["timeInHeader"] = mail.date.description + event["bodyLength"] = (mail.body)?.count + event["isEncrypted"] = mail.isEncrypted + event["decryptedBodyLength"] = (mail.decryptedBody ?? "").count + event["decryptedWithOldPrivateKey"] = mail.decryptedWithOldPrivateKey + event["isSigned"] = mail.isSigned + event["isCorrectlySigned"] = mail.isCorrectlySigned + event["x-Mailer"] = mail.xMailer + //TODO: + //event["signingKeyID"] = Logger.resolve(keyID: signingKeyID) + //event["myKeyID"] = Logger.resolve(keyID: myKeyID) + + + + //event["secureAddresses"] = secureAddresses //could mean the addresses, in this mail we have a key for + //event["encryptedForKeyIDs"] = Logger.resolve(keyIDs: encryptedForKeyIDs) + + event["trouble"] = mail.trouble + event["folder"] = LoggerDetail.resolve(folder: mail.folder) + + return event + } + + static fileprivate func extract(from mail: EphemeralMail, event: [String: Any]) -> [String: Any] { + var event = event + event["to"] = LoggerDetail.resolve(mailAddresses: mail.to) + event["cc"] = LoggerDetail.resolve(mailAddresses: mail.cc ?? NSSet()) + event["bcc"] = LoggerDetail.resolve(mailAddresses: mail.bcc ?? NSSet()) + event["communicationState"] = LoggerDetail.communicationState(subject: mail.subject ?? "") + event["specialMail"] = LoggerDetail.specialMail(subject: mail.subject ?? "") + event["bodyLength"] = (mail.body)?.count + //TODO: + //event["signingKeyID"] = Logger.resolve(keyID: signingKeyID) + //event["myKeyID"] = Logger.resolve(keyID: myKeyID) + + + + //event["secureAddresses"] = secureAddresses //could mean the addresses, in this mail we have a key for + //event["encryptedForKeyIDs"] = Logger.resolve(keyIDs: encryptedForKeyIDs) + + return event + } + + static func communicationState(subject: String) -> String { + if subject == "" { + return "" + } + var oldSubject = subject + var newSubject = "" + var hasPrefix = true + + while hasPrefix { + if oldSubject.hasPrefix("Re: ") || oldSubject.hasPrefix("RE: ") || oldSubject.hasPrefix("re: ") || oldSubject.hasPrefix("AW: ") || oldSubject.hasPrefix("Aw: ") || oldSubject.hasPrefix("aw: ") { + newSubject += "Re: " + oldSubject = String(oldSubject[oldSubject.index(oldSubject.startIndex, offsetBy: 4)...]) //damn swift3! + } else if oldSubject.hasPrefix("Fwd: ") || oldSubject.hasPrefix("FWD: ") || oldSubject.hasPrefix("fwd: ") { + newSubject += "Fwd: " + oldSubject = String(oldSubject[oldSubject.index(oldSubject.startIndex, offsetBy: 5)...]) + } else if oldSubject.hasPrefix("WG: ") || oldSubject.hasPrefix("Wg: ") || oldSubject.hasPrefix("wg: ") { + newSubject += "Fwd: " + oldSubject = String(oldSubject[oldSubject.index(oldSubject.startIndex, offsetBy: 4)...]) + } else { + hasPrefix = false + } + } + + return newSubject + } + + static func specialMail(subject: String) -> String { + if subject.contains(NSLocalizedString("inviteSubject", comment: "subject of invitation email")) { + return "invitation" + } + return "" + } + + //takes backendFolderPath + static func resolve(folder: Folder) -> String { + let folderPath = folder.path + if folderPath == UserManager.backendSentFolderPath { + return "sent" + } + if folderPath == UserManager.backendDraftFolderPath { + return "draft" + } + if folderPath == UserManager.backendInboxFolderPath { + return "inbox" + } + if folderPath == UserManager.backendTrashFolderPath { + return "trash" + } + if folderPath == UserManager.backendArchiveFolderPath { + return "archive" + } + return folder.pseudonym + } + + //get an pseudonym for a mailAddress + static func resolve(mailAddress: MailAddress) -> String { + if let addr = mailAddress as? Mail_Address { + return resolve(mail_address: addr) + } else if mailAddress is CNMailAddressExtension { + return "CNMailAddress" + } + return "unknownMailAddressType" + } + + static func resolve(mail_address: Mail_Address) -> String { + if mail_address.isUser { + return "self" + } + return mail_address.pseudonym + } + + static func resolve(mailAddresses: NSSet) -> [String] { + var result: [String] = [] + for addr in mailAddresses { + if let addr = addr as? Mail_Address { + result.append(resolve(mail_address: addr)) + } else if addr is CNMailAddressExtension { + result.append("CNMailAddress") + } else { + result.append("unknownMailAddressType") + } + } + return result + } + + static func resolve(mailAddresses: [Mail_Address]) -> [String] { + var result: [String] = [] + for addr in mailAddresses { + result.append(resolve(mail_address: addr)) + } + return result + } + + static func resolve(mailAddresses: [MailAddress]) -> [String] { + var result: [String] = [] + for addr in mailAddresses { + if let addr = addr as? Mail_Address { + result.append(resolve(mail_address: addr)) + } else if addr is CNMailAddressExtension { + result.append("CNMailAddress") + } else { + result.append("unknownMailAddressType") + } + } + return result + } + + //get an pseudonym for a keyID + static func resolve(keyID: String) -> String { + if let key = DataHandler.handler.findKey(keyID: keyID) { + return key.pseudonym + } + return "noKeyID" + } + + static func resolve(key: PersistentKey) -> String { + return key.pseudonym + } + + static func resolve(keyIDs: [String]) -> [String] { + var result: [String] = [] + for id in keyIDs { + result.append(resolve(keyID: id)) + } + return result + } + + static func saveToDisk(json: String, fileName: String = defaultFileName) { + + if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + + let fileURL = dir.appendingPathComponent(fileName) + + if FileManager.default.fileExists(atPath: fileURL.path) { + // append + do { + let fileHandle = try FileHandle(forUpdating: fileURL) + + fileHandle.seekToEndOfFile() + if let encoded = "\n\(json),".data(using: .utf8) { + fileHandle.write(encoded) + } + fileHandle.closeFile() + } + catch { + print("Error while appending to logfile: \(error.localizedDescription)") + } + } else { + // write new + do { + try json.write(to: fileURL, atomically: false, encoding: .utf8) + } + catch { + print("Error while writing logfile: \(error.localizedDescription)") + } + } + + } + } + + static func sendLog(fileName: String = defaultFileName, logMailAddress: String = logReceiver) { + + if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + + let fileURL = dir.appendingPathComponent(fileName) + + // reading + do { + let currentContent = try String(contentsOf: fileURL, encoding: .utf8) + if !currentContent.isEmpty { + AppDelegate.getAppDelegate().mailHandler.send([logMailAddress], ccEntrys: [], bccEntrys: [], subject: "[Enzevalos] Log", message: "{\"studyID\":\"" + studyID + "\",\"data\":" + "[" + currentContent.dropLast() + "\n]" + "}", callback: sendCallback, loggingMail: true) + } + } + catch { + print("Error while reading logfile: \(error.localizedDescription)") + } + } + } + + static func sendCallback(error: Error?) { + guard error == nil else { + print(error!) + return + } + + clearLog() + let tmpNextDeadline = Date(timeIntervalSinceNow: TimeInterval(loggingInterval)) + nextDeadline = tmpNextDeadline + UserManager.storeUserValue(nextDeadline as AnyObject?, attribute: Attribute.nextDeadline) + } + + static func clearLog(fileName: String = defaultFileName) { + if let dir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + + let fileURL = dir.appendingPathComponent(fileName) + + do { + try String().write(to: fileURL, atomically: false, encoding: .utf8) + } + catch { + print("Error while clearing logfile: \(error.localizedDescription)") + } + } + } +} diff --git a/enzevalos_iphone/MailHandler.swift b/enzevalos_iphone/MailHandler.swift index 64023b833fd1fbf8b02bfffc80e166bdd9f1ce10..ebc5e985cd789adebefe802f0878437d764b81ce 100644 --- a/enzevalos_iphone/MailHandler.swift +++ b/enzevalos_iphone/MailHandler.swift @@ -469,14 +469,15 @@ class MailHandler { completionCallback(MailServerConnectionError.NoData) return } - let requestKind = MCOIMAPMessagesRequestKind(rawValue: MCOIMAPMessagesRequestKind.headers.rawValue | MCOIMAPMessagesRequestKind.flags.rawValue) - let fetchOperation: MCOIMAPFetchMessagesOperation = self.IMAPSession!.fetchMessagesOperation(withFolder: folderPath, requestKind: requestKind, uids: uids) - fetchOperation.extraHeaders = MailHandler.extraHeaders - - if uids.count() == 0 { + guard uids.count() > 0 else { completionCallback(nil) return } + // First fetch -> get flags + let requestKind = MCOIMAPMessagesRequestKind(rawValue: MCOIMAPMessagesRequestKind.headers.rawValue | MCOIMAPMessagesRequestKind.flags.rawValue) + let fetchOperation: MCOIMAPFetchMessagesOperation = self.IMAPSession!.fetchMessagesOperation(withFolder: folderPath, requestKind: requestKind, uids: uids) + fetchOperation.extraHeaders = MailHandler.extraHeaders + fetchOperation.start { (err, msg, vanished) -> Void in guard err == nil else { let connerror = MailServerConnectionError.findErrorCode(error: err!) @@ -488,7 +489,7 @@ class MailHandler { let dispatchGroup = DispatchGroup() for message in msgs.reversed() { dispatchGroup.enter() - let op = self.IMAPSession?.fetchParsedMessageOperation(withFolder: folderPath, uid: message.uid) + let op = self.IMAPSession?.fetchMessageOperation(withFolder: folderPath, uid: message.uid) op?.start { err, data in guard err == nil else { let connerror = MailServerConnectionError.findErrorCode(error: err!) @@ -496,9 +497,9 @@ class MailHandler { return } if let parser = data { - let incomingMail = IncomingMail(rawData: parser.data(), uID: UInt64(message.uid), folderPath: folderPath, flags: message.flags) + let id = UInt64(message.uid) + let incomingMail = IncomingMail(rawData: parser, uID: id, folderPath: folderPath, flags: message.flags) _ = incomingMail.store(keyRecord: record) - } dispatchGroup.leave() } @@ -521,8 +522,6 @@ class MailHandler { } } - - public func getRepeals(for keyID: String) -> [Repeal] { let mails = DataHandler.handler.allMailsInFolder(key: keyID, contact: nil, folder: DataHandler.handler.findFolder(with: TravelHandler.keyManagementFolder), isSecure: true) var repeals: [Repeal] = [] @@ -973,65 +972,93 @@ class MailHandler { completionCallback(MailServerConnectionError.NoData) return } - let folderstatus = IMAPSession?.folderStatusOperation(folder.path) - folderstatus?.start { (error, status) -> Void in - guard error == nil else { - let conerror = MailServerConnectionError.findErrorCode(error: error!) - self.errorhandling(error: conerror, originalCall: {self.updateFolder(folder: folder, completionCallback: completionCallback)}, completionCallback: completionCallback) - self.IMAPSes = nil; - return - } - if let status = status { - let uidValidity = status.uidValidity - folder.uidvalidity = uidValidity - if let date = folder.lastUpdate { - self.loadMailsSinceDate(folder: folder, since: date, completionCallback: completionCallback) - } else { - if folder.path == UserManager.backendInboxFolderPath || folder.path.lowercased() == "INBOX".lowercased() { - self.initInbox(inbox: folder, completionCallback: completionCallback) - } else { - self.initFolder(folder: folder, completionCallback: completionCallback) - } - } + if folder.mailsOfFolder.count > 0 { + self.loadMailsByNum(folder: folder, completionCallback: completionCallback, multipleMails: false) + } else { + if folder.path == UserManager.backendInboxFolderPath || folder.path.lowercased() == "INBOX".lowercased() { + self.initInbox(inbox: folder, completionCallback: completionCallback) + } else { + self.initFolder(folder: folder, completionCallback: completionCallback) } } } - private func olderMails(folder: Folder, completionCallback: @escaping ((MailServerConnectionError?) -> ())) { - guard IMAPSession != nil else { + private func calculateIndicies(indicies: MCOIndexSet, folder: Folder, multipleMails: Bool) -> MCOIndexSet { + var factor = 1 + if multipleMails { + factor = 3 + } + let knownIds = folder.uids + let requestIds = indicies + requestIds.remove(knownIds) + let dif = requestIds.count() - UInt32(factor * MailHandler.MAXMAILS) + var range = MCORange(location: 0, length: UInt64(dif-1)) + if dif > MailHandler.MAXMAILS { + requestIds.remove(range) + } + var lastMinElem = dif + while requestIds.count() < factor * MailHandler.MAXMAILS && lastMinElem > 0 { + lastMinElem = lastMinElem - UInt32(MailHandler.MAXMAILS) + range = MCORange(location: UInt64(lastMinElem), length: UInt64(MailHandler.MAXMAILS)) + requestIds.add(range) + requestIds.remove(knownIds) + if folder.minID > UInt64(lastMinElem) { + folder.minID = UInt64(lastMinElem) + } + } + if lastMinElem < MailHandler.MAXMAILS { + range = MCORange(location: UInt64(1), length: UInt64(MailHandler.MAXMAILS)) + requestIds.add(range) + folder.minID = 1 + } + if multipleMails { + var (start, overflow) = folder.minID.subtractingReportingOverflow(UInt64(MailHandler.MAXMAILS)) + if overflow && folder.minID > 1 { + // 1 < folder.min < MailHandler.MAXMAILs -> start with uid = 1 + start = 1 + overflow = false + } + if !overflow { + range = MCORange(location: start, length: UInt64(MailHandler.MAXMAILS)) + requestIds.add(range) + folder.minID = start + } + + } + + return requestIds + } + + private func loadMailsByNum(folder: Folder, completionCallback: @escaping ((MailServerConnectionError?) -> ()), multipleMails: Bool) { + guard let session = IMAPSession else { completionCallback(MailServerConnectionError.NoData) return } - let folderPath = folder.path - if let mails = folder.mails { - var oldestDate: Date? - for m in mails { - if let mail = m as? PersistentMail { - if oldestDate == nil || mail.date < oldestDate { - oldestDate = mail.date - } + let path = folder.path + if let statusOP = session.folderStatusOperation(path) { + statusOP.start{(error, status) -> Void in + guard error == nil else { + let conerror = MailServerConnectionError.findErrorCode(error: error!) + self.errorhandling(error: conerror, originalCall: {self.loadMailsByNum(folder: folder, completionCallback: completionCallback, multipleMails: multipleMails)}, completionCallback: completionCallback) + self.IMAPSes = nil + return } - } - if let date = oldestDate { - let searchExp = MCOIMAPSearchExpression.search(before: date) - let searchOperation = self.IMAPSession?.searchExpressionOperation(withFolder: folderPath, expression: searchExp) - searchOperation?.start { (err, uids) -> Void in - guard err == nil else { - let conerror = MailServerConnectionError.findErrorCode(error: err!) - self.errorhandling(error: conerror, originalCall: {self.olderMails(folder: folder, completionCallback: completionCallback)}, completionCallback: completionCallback) - self.IMAPSes = nil - return - } - if let ids = uids { - //folder.lastUpdate = Date() - self.loadMessagesFromServer(ids, folderPath: folderPath, record: nil, completionCallback: completionCallback) - } else { - completionCallback(nil) - } + if let status = status, let ids = MCOIndexSet(range: MCORange(location: 0, length: UInt64(status.uidNext))) { + let newIds = self.calculateIndicies(indicies: ids, folder: folder, multipleMails: multipleMails) + self.loadMessagesFromServer(newIds, folderPath: path, record: nil, completionCallback: completionCallback) } - } else { - initFolder(folder: folder, completionCallback: completionCallback) } + } + + } + + private func olderMails(folder: Folder, completionCallback: @escaping ((MailServerConnectionError?) -> ())) { + guard IMAPSession != nil else { + completionCallback(MailServerConnectionError.NoData) + return + } + if let mails = folder.mails, mails.count > 0 { + loadMailsByNum(folder: folder, completionCallback: completionCallback, multipleMails: true) } else { initFolder(folder: folder, completionCallback: completionCallback) } @@ -1054,6 +1081,7 @@ class MailHandler { } if let ids = uids { folder.lastUpdate = Date() + ids.remove(folder.uids) self.loadMessagesFromServer(ids, folderPath: folderPath, maxLoad: maxLoad, record: nil, completionCallback: completionCallback) } else { completionCallback(nil) diff --git a/enzevalos_iphone/MailSession.swift b/enzevalos_iphone/MailSession.swift index ded6896fddfdd498cc169295b2c22e5c350c5828..fba31f4e58e69a8c84cc7baff6e29e976e81ca83 100644 --- a/enzevalos_iphone/MailSession.swift +++ b/enzevalos_iphone/MailSession.swift @@ -35,7 +35,8 @@ class MailServer: Comparable { 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 = 12 + private static let maxWaitingSeconds = 40 + private var hasConnection = false let sessionType: SessionType var hostname: String @@ -313,7 +314,10 @@ class MailServer: Comparable { private func startWaiting(){ DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(MailServer.maxWaitingSeconds)) { - if !self.sendCallback && !self.works{ + guard !self.hasConnection else { + return + } + if !self.sendCallback && !self.works { self.sendCallback = true self.callback(MailServerConnectionError.ConnectionError, self) } @@ -335,6 +339,7 @@ class MailServer: Comparable { } return } + self.hasConnection = true // No error: -> conntectionType is correct and server is reachable if self.possibleAuthTypes.isEmpty { self.possibleAuthTypes = MailSession.AUTHTYPE @@ -357,11 +362,13 @@ class MailServer: Comparable { // Observation: If the logger was not called -> connection type (e.g. startTLS, TLS) is wrong. if !self.loggerCalled, let newType = self.toTestConnType.popFirst() { self.connectionType = newType + self.hasConnection = true _ = self.findHost() } else if self.loggerCalled, let auth = self.possibleAuthTypes.last { self.possibleAuthTypes.removeLast() self.authType = auth + self.hasConnection = true _ = self.findHost() } else if self.toTestConnType.isEmpty { @@ -369,6 +376,7 @@ class MailServer: Comparable { } return } + self.hasConnection = true self.connectionType = session.connectionType self.authType = session.authType self.works = true @@ -376,6 +384,7 @@ class MailServer: Comparable { self.sendCallback = true self.callback(nil, self) } + print("SMTP works!") }) self.startWaiting() return true @@ -439,6 +448,7 @@ class MailServer: Comparable { self.sendCallback = true self.callback(nil, self) } + print("IMAP works!") } }) } diff --git a/enzevalos_iphone/New Group/Mailbot.swift b/enzevalos_iphone/New Group/Mailbot.swift index c43f5671218259843f4e6522a17e8fe991b9a529..d9408f7a15445251418fcddcd2089e71e1150cc8 100644 --- a/enzevalos_iphone/New Group/Mailbot.swift +++ b/enzevalos_iphone/New Group/Mailbot.swift @@ -44,7 +44,7 @@ class Mailbot { PS: Diese Nachricht wurde automatisch in Letterbox erzeugt und ist nur hier gespeichert. """ - static let welcomeBody = + static let welcomeBodyDE = """ Liebe Nutzerin, lieber Nutzer von Letterbox, @@ -59,12 +59,32 @@ class Mailbot { """ + static let welcomeBodyEn = + """ + Hi, + + Thank you for using and testing our app! This is a automatic generated mail and you can try to respond and test how to write a confidential mail. Our mail bot while response and you can get an impression about confidential communication. + + We are interested inyour opinion, experience and impression of Letterbox. Maybe you can spend a couple of minuntes (less than 10 minutes) to fill out our online survey? + You can find our survey here: userpage.fu-berlin.de/~wieseoli/umfrage/limesurvey/index.php/952145?lang=en&id=\(StudySettings.studyID) + + When you participate in our survey you support our research and the development of confidential mail communication. + + We are happy about any question, comment or story on mail encryption or Letterbox. Just write us a (confidential) mail! + + Thanks and best + Your Letterbox-mailbot Vic + + """ + + + static func firstMail() { if !StudySettings.studyMode || !StudySettings.presentFirstQuestionaireMail { return } - let subject = "Herzlich Willkommen in Letterbox" - let body = welcomeBody + let subject = "Welcome to Letterbox" + let body = welcomeBodyEn mailToParticipat(subject: subject, body: body) } diff --git a/enzevalos_iphone/Onboarding.swift b/enzevalos_iphone/Onboarding.swift index 46495007e3fab530b8c45af7fe089ac33d4951bb..9298a83eb256bd34ead6366672fddb6b2f6f56cf 100644 --- a/enzevalos_iphone/Onboarding.swift +++ b/enzevalos_iphone/Onboarding.swift @@ -25,7 +25,6 @@ class Onboarding: NSObject { override init() { super.init() } - static var textDelegate = TextFieldDelegate.init() static let defaultColor = UIColor.darkGray static let textColor = UIColor.white @@ -59,7 +58,10 @@ class Onboarding: NSObject { static var credentialFails = 0 static var testConfig = false - static var startTime: Date? + static var startTimeIMAPCheck: Date? //TODO What about SMTP? + + static var startTimeView = Date() + static var currentState = IntroState.Welcome static var showAuthType = false static var authTypeTest = 0 @@ -77,6 +79,7 @@ class Onboarding: NSObject { static func onboarding(_ errorCode: MailServerConnectionError? = nil) -> UIViewController { password.isSecureTextEntry = true doWhenDone = checkIMAPConfig + startTimeView = Date() //Background var myBounds = CGRect() @@ -130,8 +133,12 @@ class Onboarding: NSObject { intro2.iconImageView.image = UIGraphicsGetImageFromCurrentImageContext()! UIGraphicsEndImageContext() intro2.bodyLabel.textAlignment = NSTextAlignment.left - intro2.bodyLabel.textColor = UIColor.black - intro2.titleLabel.textColor = UIColor.black + + if ThemeManager.unencryptedMessageColor() != ThemeManager.defaultColor { + intro2.bodyLabel.textColor = UIColor.black + intro2.titleLabel.textColor = UIColor.black + } + let path = Bundle.main.path(forResource: "videoOnboarding2", ofType: "m4v") let url = URL.init(fileURLWithPath: path!) @@ -217,7 +224,10 @@ class Onboarding: NSObject { intro2.viewWillAppearBlock = { UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions.curveEaseIn, animations: { - vc?.view.backgroundColor = ThemeManager.unencryptedMessageColor() + let bgColor = ThemeManager.unencryptedMessageColor() + if bgColor != ThemeManager.defaultColor { + vc?.view.backgroundColor = bgColor + } vc?.view.setNeedsDisplay() }) } @@ -225,28 +235,48 @@ class Onboarding: NSObject { UIView.animate(withDuration: duration, delay: 0.05, options: UIView.AnimationOptions.curveEaseIn, animations: { if (vc?.view.backgroundColor != ThemeManager.encryptedMessageColor()) { vc?.view.backgroundColor = defaultColor - vc?.view.setNeedsDisplay() } + vc?.view.setNeedsDisplay() }) } intro1.viewWillAppearBlock = { - UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions.curveEaseIn, animations: { vc?.view.backgroundColor = ThemeManager.encryptedMessageColor(); vc?.view.setNeedsDisplay() }) + UIView.animate(withDuration: duration, delay: 0, options: UIView.AnimationOptions.curveEaseIn, animations: { + let bgColor = ThemeManager.encryptedMessageColor() + if bgColor != ThemeManager.defaultColor { + vc?.view.backgroundColor = bgColor + } + vc?.view.setNeedsDisplay() + }) } intro1.viewWillDisappearBlock = { UIView.animate(withDuration: duration, delay: 0.05, options: UIView.AnimationOptions.curveEaseIn, animations: { if (vc?.view.backgroundColor != ThemeManager.unencryptedMessageColor()) { vc?.view.backgroundColor = defaultColor - vc?.view.setNeedsDisplay() } + vc?.view.setNeedsDisplay() }) } loginViewController = vc! vc?.pageChanged = {(oldPage:Int, newPage: Int) -> () in password.isSecureTextEntry = true - Logger.log(onboardingPageTransition: oldPage, to: newPage, onboardingSection: "intro") + var nextState = IntroState.Welcome + switch newPage { + case 0: + nextState = .Welcome + case 1: + nextState = .ConfidentialExplanation + case 2: + nextState = .NotConfidentialExplanation + case 3: + nextState = .ReminderIcon + default: + nextState = .BriefInput + } + self.changeState(newState: nextState) + //Logger.log(onboardingPageTransition: oldPage, to: newPage, onboardingSection: "intro") } - Logger.log(onboardingState: "intro") + //Logger.log(onboardingState: "intro") return vc! } @@ -273,7 +303,8 @@ class Onboarding: NSObject { let vc = Onboard.OnboardingViewController(backgroundImage: background, contents: [page1])! vc.pageControl = UIPageControl.init() vc.view.backgroundColor = defaultColor - Logger.log(onboardingState: "checkServerConfig") + self.changeState(newState: .UseraddressDetail) + //Logger.log(onboardingState: "checkServerConfig") return vc } @@ -288,7 +319,8 @@ class Onboarding: NSObject { let vc = Onboard.OnboardingViewController(backgroundImage: background, contents: [page1]) vc?.pageControl = UIPageControl.init() vc?.view.backgroundColor = defaultColor - Logger.log(onboardingState: "keyHandling") + changeState(newState: .GenerateKey) + //Logger.log(onboardingState: "keyHandling") return vc! } //UI Definition @@ -523,9 +555,31 @@ class Onboarding: NSObject { let vc = Onboard.OnboardingViewController(backgroundImage: background, contents: contents) vc?.view.backgroundColor = defaultColor vc?.pageChanged = {(oldPage:Int, newPage: Int) -> () in - Logger.log(onboardingPageTransition: oldPage, to: newPage, onboardingSection: "detail") + var nextState = IntroState.Failed + switch newPage { + case 0: + nextState = IntroState.Failed + case 1: + nextState = IntroState.UseraddressDetail + case 2: + nextState = IntroState.UsernameDetail + case 3: + nextState = IntroState.IMAP + case 4: + nextState = IntroState.IMAPCon + case 5: + nextState = IntroState.SMTP + case 6: + nextState = IntroState.SMTPCon + case 7: + nextState = IntroState.EndDetail + default: + nextState = IntroState.Failed + } + changeState(newState: nextState) + //Logger.log(onboardingPageTransition: oldPage, to: newPage, onboardingSection: "detail") } - Logger.log(onboardingState: "detail") + //Logger.log(onboardingState: "detail") return vc! } @@ -541,7 +595,8 @@ class Onboarding: NSObject { let vc = Onboard.OnboardingViewController(backgroundImage: background, contents: [page1])! vc.pageControl = UIPageControl.init() vc.view.backgroundColor = defaultColor - Logger.log(onboardingState: "contact") + changeState(newState: .ContactAccess) + //Logger.log(onboardingState: "contact") return vc } @@ -660,7 +715,7 @@ class Onboarding: NSObject { let mailSession = setIMAPSession() if let imap = previousIMAP, MailServerConnectionError.wrongServerConfig(errors: imap.errors) && Onboarding.credentialFails == 2 && !mailSession.hasJsonFile && mailSession.startLongSearchOfServerConfig(hostFromAdr: true) { testConfig = true - startTime = Date() + startTimeIMAPCheck = Date() } else if isDetailedOnboarding{ checkDetailConfig(imap: true) @@ -669,11 +724,11 @@ class Onboarding: NSObject { else { if mailSession.startTestingServerConfigFromList() { testConfig = true - startTime = Date() + startTimeIMAPCheck = Date() } else if mailSession.startLongSearchOfServerConfig(hostFromAdr: !isDetailedOnboarding){ testConfig = true - startTime = Date() + startTimeIMAPCheck = Date() } else { Onboarding.credentialFails = 3 @@ -697,6 +752,7 @@ class Onboarding: NSObject { guard testConfig == false else { return } + startTimeIMAPCheck = nil let mailSession = setSMTPSession() let isDetailedOnboarding = Onboarding.credentialFails >= 3 if let smtp = previousSMTP, MailServerConnectionError.wrongServerConfig(errors: smtp.errors) && Onboarding.credentialFails == 2 && !mailSession.hasJsonFile && mailSession.startLongSearchOfServerConfig(hostFromAdr: !isDetailedOnboarding) { @@ -723,8 +779,13 @@ class Onboarding: NSObject { static func imapCompletion(imapWorks: Bool) { testConfig = false - if let start = startTime { - startTime = nil + if imapWorks { + _ = currentIMAP?.storeToUserDefaults() + checkSMTPConfig() + return + } + else if let start = startTimeIMAPCheck { + startTimeIMAPCheck = nil let duration = start.timeIntervalSinceNow if duration < TimeInterval(-10) { Onboarding.credentialFails = 3 @@ -732,11 +793,6 @@ class Onboarding: NSObject { return } } - if imapWorks { - _ = currentIMAP?.storeToUserDefaults() - checkSMTPConfig() - return - } else { Onboarding.credentialFails += 1 checkIMAPConfigUI() @@ -774,6 +830,12 @@ class Onboarding: NSObject { } return keys } + private static func changeState(newState: IntroState) { + let duration = Date().timeIntervalSince(startTimeView) + startTimeView = Date() + Logger.log(onboardingState: currentState, duration: duration) + currentState = newState + } } @@ -859,6 +921,8 @@ extension PickerDataDelegate: UIPickerViewDelegate { func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { pickedValue = rows[row] } + + } class Listener: MailSessionListener { @@ -879,3 +943,48 @@ class Listener: MailSessionListener { }) } } + +enum IntroState { + case Welcome, ConfidentialExplanation, NotConfidentialExplanation, ReminderIcon, BriefInput, GoogleLogIn, Failed, UseraddressDetail, UsernameDetail, IMAP, IMAPCon, SMTP, SMTPCon, EndDetail, GenerateKey, ContactAccess, Finish + + var name: String { + get { + switch self { + case .Welcome: + return "Welcome" + case .ConfidentialExplanation: + return "confidentialSlide" + case .NotConfidentialExplanation: + return "notConfidentialSlide" + case .ReminderIcon: + return "animationSlide" + case .BriefInput: + return "briefInputSlide" + case .IMAP: + return "imapSlide" + case .IMAPCon: + return "imapConnTypeSlide" + case .SMTP: + return "smtpSlide" + case .SMTPCon: + return "smtpConnTypeSlide" + case .UseraddressDetail: + return "useraddressDetailSlide" + case .EndDetail: + return "endDetailSlide" + case .GoogleLogIn: + return "GoogleLoginSlide" + case .Failed: + return "FailedSlide" + case .UsernameDetail: + return "UserNameDetailSlide" + case .GenerateKey: + return "GenerateKeySlide" + case .ContactAccess: + return "ContactAccessSlide" + case .Finish: + return "Finish" + } + } + } +} diff --git a/enzevalos_iphone/OutgoingMail.swift b/enzevalos_iphone/OutgoingMail.swift index c79f973ab8c53cb39141c2a6c57e582fe5663066..f37ededec8a637a2c5a7f87c1dfb1ce1fe788fbd 100644 --- a/enzevalos_iphone/OutgoingMail.swift +++ b/enzevalos_iphone/OutgoingMail.swift @@ -24,7 +24,7 @@ class OutgoingMail { private let ccEntrys: [MCOAddress] private let bccEntrys: [MCOAddress] var username = UserManager.loadUserValue(Attribute.accountname) as? String ?? "" - var useraddr = UserManager.loadUserValue(Attribute.userAddr) as? String ?? "" + var useraddr = (UserManager.loadUserValue(Attribute.userAddr) as? String ?? "").lowercased() var userDisplayName = UserManager.loadUserValue(Attribute.userDisplayName) as? String var sender: MCOAddress { diff --git a/enzevalos_iphone/ReadViewController.swift b/enzevalos_iphone/ReadViewController.swift index 29f8236717b0564f93d9d1f7bdb42fe3eac564ba..543632fb25975c1d6a878c9aaec50fd4770d6ca3 100644 --- a/enzevalos_iphone/ReadViewController.swift +++ b/enzevalos_iphone/ReadViewController.swift @@ -59,7 +59,8 @@ class ReadViewController: UITableViewController { var keyDiscoveryDate: Date? = nil var secretKeyPasswordField: UITextField? = nil - + var infoState = LogData.WarningType.none + var isNewPubKey: Bool? { guard let mail = mail, let signedKey = mail.signedKey else { return nil @@ -191,7 +192,6 @@ class ReadViewController: UITableViewController { override func willMove(toParent parent: UIViewController?) { super.willMove(toParent: parent) - if parent == nil { UIView.animate(withDuration: 0.3, animations: { self.navigationController?.navigationBar.barTintColor = ThemeManager.defaultColor }) } @@ -320,6 +320,7 @@ class ReadViewController: UITableViewController { performSegue(withIdentifier: "answerTo", sender: "reactButton") } Logger.log(reactTo: mail) + Logger.log(reactWarning: infoState, reaction: .clickButton) } @IBAction func markUnreadButton(_ sender: AnyObject) { @@ -352,38 +353,50 @@ class ReadViewController: UITableViewController { if let m = mail { let alert: UIAlertController let url: String + let opening = Date() + var icon = LogData.IndicatorButton.notConfidential + if m.trouble { alert = UIAlertController(title: NSLocalizedString("LetterDamaged", comment: "Modified email received")/*"Angerissener Brief"*/, message: NSLocalizedString("ReceiveDamagedInfo", comment: "Modefied email infotext"), preferredStyle: .alert) url = "https://userpage.fu-berlin.de/letterbox/faq.html#collapseTorn" + icon = .error } else if m.isSecure { alert = UIAlertController(title: NSLocalizedString("Letter", comment: "letter label"), message: NSLocalizedString("ReceiveSecureInfo", comment: "Letter infotext"), preferredStyle: .alert) url = "https://userpage.fu-berlin.de/letterbox/faq.html#secureMailAnswer" + icon = .confidential alert.addAction(UIAlertAction(title: NSLocalizedString("ReadMailOnOtherDevice", comment: "email is not readable on other devices"), style: .default, handler: { (action: UIAlertAction!) -> Void in - Logger.log(close: url, mail: m, action: "exportKey") + let duration = Date().timeIntervalSince(opening) + Logger.log(close: icon, mail: m, action: .exportKey, duration: duration) self.performSegue(withIdentifier: "exportKeyFromReadView", sender: nil) })) } else if m.isCorrectlySigned { alert = UIAlertController(title: NSLocalizedString("Postcard", comment: "postcard label"), message: NSLocalizedString("ReceiveInsecureInfoVerified", comment: "Postcard infotext"), preferredStyle: .alert) url = "https://userpage.fu-berlin.de/letterbox/faq.html#collapsePostcard" + icon = .signedOnly } else if m.isEncrypted && !m.unableToDecrypt { //TODO: maybe add travel hook here + icon = .encryptedOnly alert = UIAlertController(title: NSLocalizedString("Postcard", comment: "postcard label"), message: NSLocalizedString("ReceiveInsecureInfoEncrypted", comment: "Postcard infotext"), preferredStyle: .alert) url = "https://userpage.fu-berlin.de/letterbox/faq.html#collapsePostcard" } else if m.isEncrypted && m.unableToDecrypt { //TODO: add travel hook here + icon = .unableToDecrypt alert = UIAlertController(title: NSLocalizedString("Postcard", comment: "postcard label"), message: NSLocalizedString("ReceiveInsecureInfoDecryptionFailed", comment: "Postcard infotext"), preferredStyle: .alert) url = "https://userpage.fu-berlin.de/letterbox/faq.html#collapseBeginPGP" } else { + icon = .notConfidential alert = UIAlertController(title: NSLocalizedString("Postcard", comment: "postcard label"), message: NSLocalizedString("ReceiveInsecureInfo", comment: "Postcard infotext"), preferredStyle: .alert) url = "https://userpage.fu-berlin.de/letterbox/faq.html#collapsePostcard" } Logger.log(open: url, mail: m) alert.addAction(UIAlertAction(title: NSLocalizedString("MoreInformation", comment: "More Information label"), style: .default, handler: { (action: UIAlertAction!) -> Void in - Logger.log(close: url, mail: m, action: "openURL") + let duration = Date().timeIntervalSince(opening) + Logger.log(close: icon, mail: m, action: .moreInfo, duration: duration) UIApplication.shared.openURL(URL(string: url)!) })) alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { (action: UIAlertAction!) -> Void in - Logger.log(close: url, mail: m, action: "OK") + let duration = Date().timeIntervalSince(opening) + Logger.log(close: icon, mail: m, action: .close, duration: duration) })) DispatchQueue.main.async(execute: { self.present(alert, animated: true, completion: nil) @@ -465,9 +478,6 @@ class ReadViewController: UITableViewController { messageBody.text = messageBody.text?.trimmingCharacters(in: NSCharacterSet.whitespacesAndNewlines).appending("\n") // NavigationBar Icon - let iconView = UIImageView() - iconView.autoresizingMask = [.flexibleHeight, .flexibleWidth] - iconView.contentMode = .scaleAspectFit var icon: UIImage if mail.trouble { icon = StudySettings.securityIndicator.imageOfCorruptedIndicator() @@ -478,13 +488,20 @@ class ReadViewController: UITableViewController { } else { icon = StudySettings.securityIndicator.imageOfInsecureIndicator() } - iconView.image = icon + + let iconView = UIImageView(image: icon) + iconView.autoresizingMask = [.flexibleHeight, .flexibleWidth] + iconView.contentMode = .scaleAspectFit iconButton.setImage(icon, for: UIControl.State()) - + // Set color of icon + iconButton.tintColor = iconView.tintColor + + infoState = LogData.WarningType.none // Mail info text if mail.trouble { + infoState = .error infoSymbol.text = "!" - infoSymbol.textColor = ThemeManager.troubleMessageColor() + infoSymbol.textColor = ThemeManager.red infoHeadline.text = NSLocalizedString("corruptedHeadline", comment: "This mail is corrupted") infoHeadline.textColor = UIColor.black infoText.text = NSLocalizedString("corruptedText", comment: "This mail is corrupted") @@ -493,40 +510,48 @@ class ReadViewController: UITableViewController { infoCell.translatesAutoresizingMaskIntoConstraints = true } else if mail.isEncrypted && mail.unableToDecrypt { if TravelHandler.instance().mode != .atHome { + infoState = .unableToDecryptTravel infoSymbol.text = "?" - infoSymbol.textColor = ThemeManager.unencryptedMessageColor() + infoSymbol.textColor = ThemeManager.orange infoHeadline.text = NSLocalizedString("couldNotDecryptTravelHeadline", comment: "Message could not be decrypted") infoHeadline.textColor = UIColor.gray infoText.text = NSLocalizedString("couldNotDecryptTravelText", comment: "Message could not be decrypted") } else { //TODO: add travel hook here + infoState = .unableToDecrypt infoSymbol.text = "?" - infoSymbol.textColor = ThemeManager.unencryptedMessageColor() + infoSymbol.textColor = ThemeManager.orange infoHeadline.text = NSLocalizedString("couldNotDecryptHeadline", comment: "Message could not be decrypted") infoHeadline.textColor = UIColor.gray infoText.text = NSLocalizedString("couldNotDecryptText", comment: "Message could not be decrypted") } } else if (isNewPubKey ?? false) && !deletedWhileTravel { + infoState = .newKey infoSymbol.text = "!" - infoSymbol.textColor = ThemeManager.unencryptedMessageColor() + infoSymbol.textColor = ThemeManager.orange infoHeadline.text = NSLocalizedString("newKeyHeadline", comment: "Message contained a new public key") infoHeadline.textColor = UIColor.gray infoText.text = NSLocalizedString("newKeyText", comment: "Message contained a new public key") } else if mail.from.hasKey && !mail.isSecure { //TODO: add travel hook here + infoState = .notConfidential infoSymbol.text = "?" infoSymbol.textColor = ThemeManager.unencryptedMessageColor() infoHeadline.text = NSLocalizedString("encryptedBeforeHeadline", comment: "The sender has encrypted before") infoHeadline.textColor = UIColor.gray infoText.text = NSLocalizedString("encryptedBeforeText", comment: "The sender has encrypted before") } else if deletedWhileTravel { + infoState = .deletedWhileTravel infoSymbol.text = "!" - infoSymbol.textColor = ThemeManager.encryptedMessageColor() + infoSymbol.textColor = ThemeManager.green infoHeadline.text = NSLocalizedString("unreadableWhileTravelHeadline", comment: "The mail is not readable while traveling") infoHeadline.textColor = UIColor.gray infoText.text = NSLocalizedString("unreadableWhileTravelText", comment: "The mail is not readable while traveling") } + if infoState != .none { + Logger.log(warning: infoState) + } } } diff --git a/enzevalos_iphone/SendViewController.swift b/enzevalos_iphone/SendViewController.swift index 06ebc37462870f1a0f82eb69d2e3c1fe89236fa6..ea331c0bc08f55e05d428804f6c1353ed6ab1818 100644 --- a/enzevalos_iphone/SendViewController.swift +++ b/enzevalos_iphone/SendViewController.swift @@ -50,7 +50,7 @@ class SendViewController: UIViewController { @IBOutlet weak var scrollViewBottom: NSLayoutConstraint! @IBOutlet weak var scrollviewRecognizer: UITapGestureRecognizer! @IBOutlet weak var sendButton: UIBarButtonItem! - + var keyboardOpened = false var keyboardY: CGFloat = 0 var keyboardHeight: CGFloat = 0 @@ -137,8 +137,14 @@ class SendViewController: UIViewController { subjectText.toLabelText = NSLocalizedString("Subject", comment: "subject label") + ": " - let iconView = AnimatedSendIcon() - iconView.frame = iconView.frame.offsetBy(dx: 0, dy: -10) + var iconView: SendIcon & UIView = SimpleSendIcon() + if StudySettings.showBothSecurityIconsInSend { + iconView = AnimatedSendIcon() + iconView.frame = iconView.frame.offsetBy(dx: 0, dy: -10) + } + else { + iconView.frame = iconView.frame.offsetBy(dx: 10, dy: 0) + } iconButton.addSubview(iconView) toText.delegate = dataDelegate @@ -662,7 +668,7 @@ class SendViewController: UIViewController { func animateIfNeeded() { var uiSecurityState: SendViewMailSecurityState = .letter - if let view = iconButton.subviews.first as? AnimatedSendIcon, view.isPostcardOnTop { + if let view = iconButton.subviews.first as? SendIcon, view.isPostcardOnTop { uiSecurityState = .postcard } @@ -684,7 +690,7 @@ class SendViewController: UIViewController { } func startIconAnimation() { - if let view = iconButton.subviews.first as? AnimatedSendIcon { + if let view = iconButton.subviews.first as? SendIcon { view.switchIcons() } } @@ -693,8 +699,11 @@ class SendViewController: UIViewController { let alert: UIAlertController let url: String let travelMode = TravelHandler.instance().mode + var icon = LogData.IndicatorButton.notConfidential + let opening = Date() if mailSecurityState != .letter && travelMode != .borderCrossing { alert = UIAlertController(title: NSLocalizedString("Postcard", comment: "Postcard label"), message: enforcePostcard ? NSLocalizedString("SendInsecureInfoAll", comment: "Postcard infotext") : NSLocalizedString("SendInsecureInfo", comment: "Postcard infotext"), preferredStyle: .alert) + icon = .notConfidential url = "https://userpage.fu-berlin.de/letterbox/faq.html#headingPostcard" if subjectText.inputText() != NSLocalizedString("inviteSubject", comment: "") && !UserDefaults.standard.bool(forKey: "hideFreeTextInvitation") { alert.addAction(UIAlertAction(title: freeTextInviationTitle, style: .default, handler: { @@ -709,8 +718,8 @@ class SendViewController: UIViewController { self.showHelpDialog() } } - - Logger.log(close: url, mail: nil, action: "invitationButton in mode \(StudySettings.invitationsmode)") + let duration = Date().timeIntervalSince(opening) + Logger.log(close: icon, mail: nil, action: .inviteOther, duration: duration) })) } } else if travelMode == .borderCrossing { @@ -719,24 +728,42 @@ class SendViewController: UIViewController { alert.addAction(UIAlertAction(title: NSLocalizedString("BorderCrossed", comment: "The user has passed the border"), style: .default, handler: { (action: UIAlertAction) -> Void in let travelHandler = TravelHandler.instance() - Logger.log(close: url, mail: nil, action: "travelmode follow-up") + let duration = Date().timeIntervalSince(opening) + Logger.log(close: icon, mail: nil, action: .travelFollowUp, duration: duration) self.navigationController?.navigationBar.barTintColor = ThemeManager.defaultColor self.navigationController?.pushViewController(travelHandler.getUI(current: self), animated: true) })) } else { alert = UIAlertController(title: NSLocalizedString("Letter", comment: "Letter label"), message: NSLocalizedString("SendSecureInfo", comment: "Letter infotext"), preferredStyle: .alert) url = "https://userpage.fu-berlin.de/letterbox/faq.html#secureMail" + icon = .confidential } if recipientSecurityState != .allInsecure && travelMode != .borderCrossing { + icon = .notConfidential if enforcePostcard { - alert.addAction(UIAlertAction(title: recipientSecurityState == .allInsecure || recipientSecurityState == .mixed ? NSLocalizedString("sendSecureIfPossible", comment: "This mail should be send securely to people with keys") : NSLocalizedString("sendSecure", comment: "This mail should be send securely"), style: .default, handler: { (action: UIAlertAction!) -> Void in - Logger.log(close: url, mail: nil, action: "sendSecureIfPossible") + var title = NSLocalizedString("sendSecure", comment: "This mail should be send securely") + var actionType = LogData.UserInteractionPopUp.EnableEnforceConf + if recipientSecurityState == .allInsecure || recipientSecurityState == .mixed { + title = NSLocalizedString("sendSecureIfPossible", comment: "This mail should be send securely to people with keys") + actionType = .DisableEnforceConf + } + alert.addAction(UIAlertAction(title: title , style: .default, handler: { (action: UIAlertAction!) -> Void in + let duration = Date().timeIntervalSince(opening) + Logger.log(close: icon, mail: nil, action: actionType, duration: duration) self.enforcePostcard = false DispatchQueue.main.async { self.updateSecurityUI() } })) } else { - alert.addAction(UIAlertAction(title: recipientSecurityState == .allInsecure || recipientSecurityState == .mixed ? NSLocalizedString("sendInsecureAll", comment: "This mail should be send insecurely to everyone, including contacts with keys") : NSLocalizedString("sendInsecure", comment: "This mail should be send insecurely"), style: .default, handler: { (action: UIAlertAction!) -> Void in - Logger.log(close: url, mail: nil, action: "sendInsecure") + icon = .confidential + var title = NSLocalizedString("sendInsecure", comment: "This mail should be send insecurely") + var actionType = LogData.UserInteractionPopUp.disableConf + if recipientSecurityState == .allInsecure || recipientSecurityState == .mixed { + title = NSLocalizedString("sendInsecureAll", comment: "This mail should be send insecurely to everyone, including contacts with keys") + actionType = .enableConf + } + alert.addAction(UIAlertAction(title: title, style: .default, handler: { (action: UIAlertAction!) -> Void in + let duration = Date().timeIntervalSince(opening) + Logger.log(close: icon, mail: nil, action: actionType, duration: duration) self.enforcePostcard = true DispatchQueue.main.async { self.updateSecurityUI() } })) @@ -744,11 +771,13 @@ class SendViewController: UIViewController { } Logger.log(open: url, mail: nil) alert.addAction(UIAlertAction(title: NSLocalizedString("MoreInformation", comment: "More Information label"), style: .default, handler: { (action: UIAlertAction!) -> Void in - Logger.log(close: url, mail: nil, action: "openURL") + let duration = Date().timeIntervalSince(opening) + Logger.log(close: icon, mail: nil, action: .moreInfo, duration: duration) UIApplication.shared.openURL(URL(string: url)!) })) alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { (action: UIAlertAction!) -> Void in - Logger.log(close: url, mail: nil, action: "OK") + let duration = Date().timeIntervalSince(opening) + Logger.log(close: icon, mail: nil, action: .close, duration: duration) })) DispatchQueue.main.async(execute: { self.present(alert, animated: true, completion: nil) diff --git a/enzevalos_iphone/SimpleSendIcon.swift b/enzevalos_iphone/SimpleSendIcon.swift new file mode 100644 index 0000000000000000000000000000000000000000..2d796f2bfea0aa27ff85e526a82447bed34993fa --- /dev/null +++ b/enzevalos_iphone/SimpleSendIcon.swift @@ -0,0 +1,66 @@ +// +// SimpleSendIcon.swift +// enzevalos_iphone +// +// Created by Oliver on 13.04.2019. +// Copyright © 2018 fu-berlin. +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. +// +// RezisingBehavior taken from PaintCode generated file + +import Foundation +import UIKit + +protocol SendIcon { + func switchIcons() + var isPostcardOnTop: Bool { + get + } +} + +class SimpleSendIcon: UIView, SendIcon { + var isPostcardOnTop = false + + override init(frame: CGRect) { + super.init(frame: frame) + setIcon(toLetter: true) + } + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setIcon(toLetter: Bool) { + for subView in self.subviews { + self.willRemoveSubview(subView) + subView.removeFromSuperview() + } + var icon = StudySettings.securityIndicator.imageOfSecureIndicator(button: true) + isPostcardOnTop = false + if !toLetter { + // move to secure state + isPostcardOnTop = true + icon = StudySettings.securityIndicator.imageOfInsecureIndicator(button: true) + } + + let iconView = UIImageView(image: icon) + iconView.autoresizingMask = [.flexibleHeight, .flexibleWidth] + iconView.contentMode = .scaleAspectFit + self.addSubview(iconView) + self.reloadInputViews() + } + + func switchIcons() { + setIcon(toLetter: isPostcardOnTop) + } +} diff --git a/enzevalos_iphone/StudySettings.swift b/enzevalos_iphone/StudySettings.swift index 3280c3d7145a8218a1d4e115a249f55e2cf01481..c0060e09396610253be2ee04f448ffa843e876f0 100644 --- a/enzevalos_iphone/StudySettings.swift +++ b/enzevalos_iphone/StudySettings.swift @@ -37,6 +37,7 @@ class StudySettings { } static var presentFirstQuestionaireMail = false + static var showBothSecurityIconsInSend = false static var securityIndicator: SecurityIndicator = SecurityIndicator.load() as! SecurityIndicator static var invitationsmode: Inviation = Inviation.load() as! Inviation @@ -74,14 +75,15 @@ class StudySettings { Logger.logging = false return } + else { + Logger.logging = true + } if UserDefaults.standard.string(forKey: "studyID") != nil && UserDefaults.standard.string(forKey: "hideWarnings") != nil { //no need to refill this fields, they are already loaded return } - Logger.logging = true let keychain = Keychain(service: "Enzevalos/Study") if let studyID = keychain["studyID"] { UserDefaults.standard.set(studyID, forKey: "studyID") - Logger.studyID = studyID } else { let studyID = String.random(length: 30) presentFirstQuestionaireMail = true @@ -99,7 +101,7 @@ class StudySettings { } public static func setupStudyKeys() { - if studyMode || Logger.logging { + if studyMode { setupStudyPublicKeys() } } diff --git a/enzevalos_iphone/StyleKits/IconsStyleKit.swift b/enzevalos_iphone/StyleKits/IconsStyleKit.swift index 26c2b7810bb7e41bc03709b15cecdac6981fe84a..e04466625d767dc82f1433a6fd632aeca3114f50 100644 --- a/enzevalos_iphone/StyleKits/IconsStyleKit.swift +++ b/enzevalos_iphone/StyleKits/IconsStyleKit.swift @@ -19,20 +19,25 @@ internal class IconsStyleKit: NSObject { private struct Cache { static let strokeColor: UIColor = UIColor(red: 0.000, green: 0.000, blue: 0.000, alpha: 1.000) + static let buttonStrokeColor: UIColor = UIView().tintColor static var imageOfLetter: UIImage? static var imageOfLetterBG: UIImage? + static var imageOfLetterButton: UIImage? static var letterTargets: [AnyObject]? static var imageOfLetterCorrupted: UIImage? static var letterCorruptedTargets: [AnyObject]? static var imageOfPostcard: UIImage? + static var imageOfPostcardButton: UIImage? static var imageOfPostcardBG: UIImage? static var postcardTargets: [AnyObject]? static var imageOfLetterOpen: UIImage? static var letterOpenTargets: [AnyObject]? static var imageOfPadlockSecure: UIImage? + static var imageOfPadlockSecureButton: UIImage? static var imageOfPadlockSecureBG: UIImage? static var padlockSecureTargets: [AnyObject]? static var imageOfPadlockInsecure: UIImage? + static var imageOfPadlockInsecureButton: UIImage? static var imageOfPadlockInsecureBG: UIImage? static var padlockInsecureTargets: [AnyObject]? static var imageOfPadlockError: UIImage? @@ -214,7 +219,6 @@ internal class IconsStyleKit: NSObject { @objc open dynamic class func drawPostcard(frame targetFrame: CGRect = CGRect(x: 0, y: 0, width: 49, height: 34), resizing: ResizingBehavior = .aspectFit, color: UIColor = IconsStyleKit.strokeColor, fillBackground: Bool = false) { //// General Declarations let context = UIGraphicsGetCurrentContext()! - //// Resize to Target Frame context.saveGState() let resizedFrame: CGRect = resizing.apply(rect: CGRect(x: 0, y: 0, width: 49, height: 34), target: targetFrame) @@ -525,7 +529,6 @@ internal class IconsStyleKit: NSObject { context.restoreGState() context.restoreGState() - } @objc dynamic public class func drawPadlockInsecure(frame targetFrame: CGRect = CGRect(x: 0, y: 0, width: 35, height: 50), resizing: ResizingBehavior = .aspectFit, color: UIColor = strokeColor, fillBackground: Bool = false) { @@ -608,9 +611,7 @@ internal class IconsStyleKit: NSObject { @objc dynamic public class func drawPadlockError(frame targetFrame: CGRect = CGRect(x: 0, y: 0, width: 35, height: 50), resizing: ResizingBehavior = .aspectFit, color: UIColor = strokeColor, fillBackground: Bool = false) { //// General Declarations let context = UIGraphicsGetCurrentContext()! - - //// Resize to Target Frame context.saveGState() let resizedFrame: CGRect = resizing.apply(rect: CGRect(x: 0, y: 0, width: 35, height: 50), target: targetFrame) @@ -798,12 +799,7 @@ internal class IconsStyleKit: NSObject { fillColor7.setFill() bezier139Path.fill() - - - - context.restoreGState() - context.restoreGState() } @@ -823,6 +819,20 @@ internal class IconsStyleKit: NSObject { return Cache.imageOfLetter! } + + @objc open dynamic class var imageOfLetterButton: UIImage { + if Cache.imageOfLetterButton != nil { + return Cache.imageOfLetterButton! + } + + UIGraphicsBeginImageContextWithOptions(CGSize(width: 50, height: 35), false, 0) + IconsStyleKit.drawLetter(color: IconsStyleKit.Cache.buttonStrokeColor) + + Cache.imageOfLetterButton = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + + return Cache.imageOfLetterButton! + } @objc open dynamic class var imageOfLetterBG: UIImage { if Cache.imageOfLetterBG != nil { @@ -865,6 +875,21 @@ internal class IconsStyleKit: NSObject { return Cache.imageOfPostcard! } + + @objc open dynamic class var imageOfPostcardButton: UIImage { + if Cache.imageOfPostcardButton != nil { + return Cache.imageOfPostcardButton! + } + + UIGraphicsBeginImageContextWithOptions(CGSize(width: 49, height: 34), false, 0) + IconsStyleKit.drawPostcard(color: Cache.buttonStrokeColor) + + Cache.imageOfPostcardButton = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + + return Cache.imageOfPostcardButton! + } + @objc open dynamic class var imageOfPostcardBG: UIImage { if Cache.imageOfPostcardBG != nil { @@ -908,6 +933,20 @@ internal class IconsStyleKit: NSObject { return Cache.imageOfPadlockSecure! } + @objc dynamic public class var imageOfPadlockSecureButton: UIImage { + if Cache.imageOfPadlockSecureButton != nil { + return Cache.imageOfPadlockSecureButton! + } + + UIGraphicsBeginImageContextWithOptions(CGSize(width: height, height: width), false, 0) + IconsStyleKit.drawPadlockSecure(color: Cache.buttonStrokeColor, fillBackground: false) + + Cache.imageOfPadlockSecureButton = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + + return Cache.imageOfPadlockSecureButton! + } + @objc dynamic public class var imageOfPadlockSecureBG: UIImage { if Cache.imageOfPadlockSecureBG != nil { return Cache.imageOfPadlockSecureBG! @@ -936,6 +975,20 @@ internal class IconsStyleKit: NSObject { return Cache.imageOfPadlockInsecure! } + @objc dynamic public class var imageOfPadlockInsecureButton: UIImage { + if Cache.imageOfPadlockInsecureButton != nil { + return Cache.imageOfPadlockInsecureButton! + } + + UIGraphicsBeginImageContextWithOptions(CGSize(width: height, height: width), false, 0) + IconsStyleKit.drawPadlockInsecure(color: Cache.buttonStrokeColor ,fillBackground: false) + + Cache.imageOfPadlockInsecureButton = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + + return Cache.imageOfPadlockInsecureButton! + } + @objc dynamic public class var imageOfPadlockInsecureBG: UIImage { if Cache.imageOfPadlockInsecureBG != nil { return Cache.imageOfPadlockInsecureBG! diff --git a/enzevalos_iphone/Theme.swift b/enzevalos_iphone/Theme.swift index ed06dfd185ca210efe28ae38358574e58df43b77..21dc7bf5b4b9f22cf36ea86a481d44c724575338 100644 --- a/enzevalos_iphone/Theme.swift +++ b/enzevalos_iphone/Theme.swift @@ -24,6 +24,7 @@ let SelectedThemeKey = "security_indicator" let defaultColor = ThemeManager.defaultColor enum Theme: Int { + /* Our themes: No security indicators -> no different colors, symbols @@ -43,7 +44,7 @@ enum Theme: Int { default: // orange // return UIColor(red: 247/255, green: 185/255, blue: 0/255, alpha: 1.0) - return UIColor(red: 255 / 255, green: 204 / 255, blue: 0 / 255, alpha: 1.0) + return ThemeManager.orange } } @@ -57,7 +58,7 @@ enum Theme: Int { default: // green // return UIColor(red: 115/255, green: 229/255, blue: 105/255, alpha: 1.0) - return UIColor(red: 76 / 255, green: 217 / 255, blue: 100 / 255, alpha: 1.0) + return ThemeManager.green } } @@ -69,20 +70,20 @@ enum Theme: Int { return defaultColor default: // green - return UIColor(red: 76 / 255, green: 217 / 255, blue: 100 / 255, alpha: 1.0) + return ThemeManager.green } } var troubleMessageColor: UIColor { switch self { case .no_security_indicator: - return defaultColor + return ThemeManager.defaultColor case .weak_security_indicator: - return defaultColor + return ThemeManager.defaultColor default: // red // return UIColor(red: 255/255, green: 99/255, blue: 99/255, alpha: 1.0) - return UIColor(red: 255 / 255, green: 59 / 255, blue: 48 / 255, alpha: 1.0) + return ThemeManager.red } } @@ -90,11 +91,16 @@ enum Theme: Int { struct ThemeManager { + static let orange = UIColor(red: 255 / 255, green: 204 / 255, blue: 0 / 255, alpha: 1.0) + static let green = UIColor(red: 76 / 255, green: 217 / 255, blue: 100 / 255, alpha: 1.0) + static let red = UIColor(red: 255 / 255, green: 59 / 255, blue: 48 / 255, alpha: 1.0) + static let defaultColor: UIColor = UIColor(red: 249 / 255, green: 249 / 255, blue: 249 / 255, alpha: 1.0) + static func currentTheme() -> Theme { if let storedTheme = (UserDefaults.standard.value(forKey: SelectedThemeKey) as? Int) { return Theme(rawValue: storedTheme)! } else { - return .very_strong_security_indicator + return .no_security_indicator } } @@ -115,7 +121,7 @@ struct ThemeManager { } static func animation() -> Bool { - return currentTheme() == .very_strong_security_indicator + return true } static func applyTheme(_ theme: Theme) { @@ -123,6 +129,5 @@ struct ThemeManager { UserDefaults.standard.synchronize() } - static var defaultColor: UIColor = UIColor(red: 249 / 255, green: 249 / 255, blue: 249 / 255, alpha: 1.0) } diff --git a/enzevalos_iphone/UserData.swift b/enzevalos_iphone/UserData.swift index 6da46ead183e45a06630b986651ef34510b83e17..c86d1ea2074b99c3bafc6fb3dbfc5d36c02b528d 100644 --- a/enzevalos_iphone/UserData.swift +++ b/enzevalos_iphone/UserData.swift @@ -190,7 +190,16 @@ struct UserManager { let value = UserDefaults.standard.value(forKey: "\(attribute.rawValue)") if value != nil { return value as AnyObject? - } else { + } + if attribute == .accountname { + // fix bug when accountname is missing + if let value = loadUserValue(_:.userAddr) { + storeUserValue(value, attribute: .accountname) + return value + } + return attribute.defaultValue + } + else { _ = storeUserValue(attribute.defaultValue, attribute: attribute) return attribute.defaultValue } diff --git a/enzevalos_iphone/enzevalos_iphone.xcdatamodeld/enzevalos_iphone 6.xcdatamodel/contents b/enzevalos_iphone/enzevalos_iphone.xcdatamodeld/enzevalos_iphone 6.xcdatamodel/contents index 712ada75e6494aa3c3aa143334503e0eb83c007e..cc6a8a2d1abaac670e7735144bd5c5683e4f5a07 100644 --- a/enzevalos_iphone/enzevalos_iphone.xcdatamodeld/enzevalos_iphone 6.xcdatamodel/contents +++ b/enzevalos_iphone/enzevalos_iphone.xcdatamodeld/enzevalos_iphone 6.xcdatamodel/contents @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8" standalone="yes"?> -<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14135" systemVersion="17G4015" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier=""> +<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="17G6030" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier=""> <entity name="Account" representedClassName="Account" syncable="YES" codeGenerationType="class"> <attribute name="archiveFolderPath" attributeType="String" syncable="YES"/> <attribute name="displayName" attributeType="String" syncable="YES"/> @@ -42,19 +42,21 @@ <attribute name="icon" optional="YES" attributeType="String" syncable="YES"/> <attribute name="lastUpdate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/> <attribute name="maxID" optional="YES" attributeType="Decimal" defaultValueString="1" syncable="YES"/> + <attribute name="minUID" optional="YES" attributeType="Decimal" defaultValueString="0.0" syncable="YES"/> <attribute name="path" attributeType="String" syncable="YES"/> <attribute name="pseudonym" attributeType="String" syncable="YES"/> <attribute name="uidvalidity" optional="YES" attributeType="Decimal" defaultValueString="0.0" syncable="YES"/> <relationship name="keyRecords" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="KeyRecord" inverseName="folder" inverseEntity="KeyRecord" syncable="YES"/> - <relationship name="mails" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PersistentMail" inverseName="folder" inverseEntity="PersistentMail" syncable="YES"/> + <relationship name="mails" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PersistentMail" inverseName="folder" inverseEntity="PersistentMail" syncable="YES"/> <relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Folder" inverseName="subfolder" inverseEntity="Folder" syncable="YES"/> <relationship name="subfolder" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Folder" inverseName="parent" inverseEntity="Folder" syncable="YES"/> </entity> <entity name="KeyRecord" representedClassName="KeyRecord" syncable="YES"> + <attribute name="newestDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/> <relationship name="contact" maxCount="1" deletionRule="Nullify" destinationEntity="EnzevalosContact" inverseName="keyrecords" inverseEntity="EnzevalosContact" syncable="YES"/> <relationship name="folder" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Folder" inverseName="keyRecords" inverseEntity="Folder" syncable="YES"/> <relationship name="key" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PersistentKey" inverseName="record" inverseEntity="PersistentKey" syncable="YES"/> - <relationship name="persistentMails" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="PersistentMail" inverseName="record" inverseEntity="PersistentMail" syncable="YES"/> + <relationship name="persistentMails" optional="YES" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="PersistentMail" inverseName="record" inverseEntity="PersistentMail" syncable="YES"/> </entity> <entity name="Mail_Address" representedClassName="Mail_Address" syncable="YES"> <attribute name="address" attributeType="String" defaultValueString="""" syncable="YES"/> @@ -153,14 +155,15 @@ <relationship name="imap" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="imap" inverseEntity="Account" syncable="YES"/> <relationship name="smtp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Account" inverseName="smtp" inverseEntity="Account" syncable="YES"/> </entity> + <fetchRequest name="allKeyRecords" entity="KeyRecord" predicateString="NOT (FALSEPREDICATE)" returnDistinctResults="YES"/> <fetchRequest name="getFolder" entity="Folder" predicateString="name == "$folder""/> <fetchRequest name="getMailAddress" entity="Mail_Address" predicateString="address == "$adr""/> <elements> <element name="Account" positionX="-315" positionY="-36" width="128" height="255"/> <element name="Attachment" positionX="-315" positionY="-36" width="128" height="210"/> <element name="EnzevalosContact" positionX="-209" positionY="198" width="128" height="120"/> - <element name="Folder" positionX="-297" positionY="-18" width="128" height="225"/> - <element name="KeyRecord" positionX="-315" positionY="-36" width="128" height="105"/> + <element name="Folder" positionX="-297" positionY="-18" width="128" height="240"/> + <element name="KeyRecord" positionX="-315" positionY="-36" width="128" height="30"/> <element name="Mail_Address" positionX="-297" positionY="-18" width="128" height="210"/> <element name="PersistentKey" positionX="-315" positionY="-36" width="128" height="390"/> <element name="PersistentMail" positionX="-416" positionY="-189" width="128" height="30"/> diff --git a/enzevalos_iphone/mail/IncomingMail.swift b/enzevalos_iphone/mail/IncomingMail.swift index e24abaddbc26f241f3c17c9ec292679a225192d0..20b654120b2d291c37811826937e094dfd2c7bd3 100644 --- a/enzevalos_iphone/mail/IncomingMail.swift +++ b/enzevalos_iphone/mail/IncomingMail.swift @@ -242,7 +242,7 @@ class IncomingMail { } func store(keyRecord: KeyRecord?) -> PersistentMail? { - let sk = secretKeys.first //TODO FIX + let sk = secretKeys.first //TODO FIX: may import more secret keys? let mail = DataHandler.handler.createMail(uID, sender: from, receivers: rec, cc: cc, time: date, received: true, subject: subject, body: body, readableAttachments: readableAttachments, flags: flags, record: keyRecord, autocrypt: autocrypt, decryptedData: cryptoObj, folderPath: folderPath, secretKey: sk, references: references, mailagent: userAgent, messageID: msgID, encryptedBody: encryptedBody, storeEncrypted: storeEncrypted) if let m = mail { let pgp = SwiftPGP() diff --git a/enzevalos_iphone/study parameters/SecurityIndicator.swift b/enzevalos_iphone/study parameters/SecurityIndicator.swift index 8c1489cf932d7396d21cdf80daa73b48d5066440..aa25e283f21ca62d96a1a17dd09dd25abd470766 100644 --- a/enzevalos_iphone/study parameters/SecurityIndicator.swift +++ b/enzevalos_iphone/study parameters/SecurityIndicator.swift @@ -62,9 +62,12 @@ enum SecurityIndicator: Int, StudyParameterProtocol { - func imageOfSecureIndicator(background: Bool = false, open: Bool = false) -> UIImage { + func imageOfSecureIndicator(background: Bool = false, open: Bool = false, button: Bool = false) -> UIImage { switch self { case .letter: + if button { + return IconsStyleKit.imageOfLetterButton + } if open { return IconsStyleKit.imageOfLetterOpen } @@ -73,6 +76,9 @@ enum SecurityIndicator: Int, StudyParameterProtocol { } return IconsStyleKit.imageOfLetter case .padlock: + if button { + return IconsStyleKit.imageOfPadlockSecureButton + } if background { return IconsStyleKit.imageOfPadlockSecureBG } @@ -90,14 +96,20 @@ enum SecurityIndicator: Int, StudyParameterProtocol { } } - func imageOfInsecureIndicator(background: Bool = false) -> UIImage { + func imageOfInsecureIndicator(background: Bool = false, button: Bool = false) -> UIImage { switch self { case .letter: + if button { + return IconsStyleKit.imageOfPostcardButton + } if background { return IconsStyleKit.imageOfPostcardBG } return IconsStyleKit.imageOfPostcard case .padlock: + if button { + return IconsStyleKit.imageOfPadlockInsecureButton + } if background { return IconsStyleKit.imageOfPadlockInsecureBG } diff --git a/enzevalos_iphone/study parameters/StudyParameterProtocol.swift b/enzevalos_iphone/study parameters/StudyParameterProtocol.swift index 72e54d75544a67795c62268a8ac9a806999713a5..6f449b6dca61f44137bf1ec4c7fc08038e6af9e3 100644 --- a/enzevalos_iphone/study parameters/StudyParameterProtocol.swift +++ b/enzevalos_iphone/study parameters/StudyParameterProtocol.swift @@ -36,7 +36,6 @@ extension StudyParameterProtocol { try keychain.remove(self.keyName) } catch { - print(error) return false } diff --git a/enzevalos_iphoneTests/CoreDataTests.swift b/enzevalos_iphoneTests/CoreDataTests.swift index 68ba9eb36e4b6c65aaea3c1263cc8a07364fa71c..2c2dbae7ebe7c4c3b3ca63fdce0e95b26bed918b 100644 --- a/enzevalos_iphoneTests/CoreDataTests.swift +++ b/enzevalos_iphoneTests/CoreDataTests.swift @@ -312,4 +312,24 @@ class CoraDataTests: XCTestCase { return mail } + + func testKeysPerAddress() { + let adrKey = "key@example.com" + let adrNoKey = "noKey@example.com" + let adrNoKey2 = "noKey2@example.com" + var mailAdrKey = datahandler.getMailAddress(adrKey, temporary: false) + let mailAdrNoKey = datahandler.getMailAddress(adrNoKey, temporary: false) + _ = datahandler.getMailAddress(adrNoKey2, temporary: false) + XCTAssertFalse(mailAdrKey.hasKey) + XCTAssertFalse(mailAdrNoKey.hasKey) + XCTAssertEqual(datahandler.getAddresses().count, 3) + _ = datahandler.newPublicKey(keyID: "000", cryptoType: .PGP, adr: adrKey, autocrypt: false) + _ = datahandler.newPublicKey(keyID: "001", cryptoType: .PGP, adr: adrKey, autocrypt: false) + mailAdrKey = datahandler.getMailAddress(adrKey, temporary: false) + XCTAssertTrue(mailAdrKey.hasKey) + let withKeys = datahandler.findMailAddress(withKey: true).count + let noKeys = datahandler.findMailAddress(withKey: false).count + XCTAssertEqual(withKeys, 1, "Adresses with key: \(withKeys)") + XCTAssertEqual(noKeys, 2, "Adresses without key: \(noKeys)") + } } diff --git a/enzevalos_iphoneUITests/OnboardingTest.swift b/enzevalos_iphoneUITests/OnboardingTest.swift new file mode 100644 index 0000000000000000000000000000000000000000..887258d652e70cf2d8237d2d4e9473ef129eb8d8 --- /dev/null +++ b/enzevalos_iphoneUITests/OnboardingTest.swift @@ -0,0 +1,40 @@ +// +// OnboardingTest.swift +// enzevalos_iphoneUITests +// +// Created by Oliver Wiese on 21.05.19. +// Copyright © 2019 fu-berlin. All rights reserved. +// + +import XCTest + +class OnboardingTest: XCTestCase { + var app: XCUIApplication! + + override func setUp() { + // Put setup code here. This method is called before the invocation of each test method in the class. + app = XCUIApplication() + app.launch() + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + + func testLogging() { + // Use recording to get started writing UI tests. + // Use XCTAssert and related functions to verify your tests produce the correct results. + app.otherElements.containing(.pageIndicator, identifier:"page 1 of 5").children(matching: .other).element.children(matching: .other).element.children(matching: .other).element.children(matching: .other).element.swipeLeft() + + app.otherElements.containing(.pageIndicator, identifier:"page 2 of 5").children(matching: .other).element.children(matching: .other).element.children(matching: .other).element.children(matching: .other).element.swipeLeft() + app/*@START_MENU_TOKEN@*/.staticTexts["OnboardSubTextAccessibilityIdentifier"]/*[[".staticTexts[\"You, the sender and both mail providers know the content of the mail but who sent the mail?\\nSender addresses can be freely chosen. A fraud can impersonate a person when using a known mail address as sender address.\"]",".staticTexts[\"OnboardSubTextAccessibilityIdentifier\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.swipeLeft() + app.otherElements["OnboardInputViewAccessibilityIdentifier"].swipeLeft() + } + +}