Skip to content
Merged
20 changes: 20 additions & 0 deletions FlowCrypt.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,12 @@
514C34DB276CE19C00FCAB79 /* ComposeMessageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DA276CE19C00FCAB79 /* ComposeMessageContext.swift */; };
514C34DD276CE1C000FCAB79 /* ComposeMessageRecipient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */; };
514C34DF276CE20700FCAB79 /* ComposeMessageService+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */; };
5152F196277E5AED00BE8A5B /* MessageUploadDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */; };
5168FB0B274F94D300131072 /* MessageAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5168FB0A274F94D300131072 /* MessageAttachment.swift */; };
51775C32270B01C200D7C944 /* PrvKeyInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51775C31270B01C200D7C944 /* PrvKeyInfoTests.swift */; };
51775C39270C7D2400D7C944 /* StorageMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51775C38270C7D2400D7C944 /* StorageMethod.swift */; };
517C2E302779F90700FECF32 /* MultipartDataRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517C2E2F2779F90700FECF32 /* MultipartDataRequest.swift */; };
517C2E33277A0C6300FECF32 /* EnterpriseServerApiError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517C2E32277A0C6300FECF32 /* EnterpriseServerApiError.swift */; };
5180CB9127356D48001FC7EF /* MessageSubjectNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */; };
5180CB9327357B67001FC7EF /* RawClientConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB9227357B67001FC7EF /* RawClientConfiguration.swift */; };
5180CB9527357BB0001FC7EF /* WkdApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5180CB9427357BB0001FC7EF /* WkdApi.swift */; };
Expand Down Expand Up @@ -494,9 +497,12 @@
514C34DA276CE19C00FCAB79 /* ComposeMessageContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageContext.swift; sourceTree = "<group>"; };
514C34DC276CE1C000FCAB79 /* ComposeMessageRecipient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMessageRecipient.swift; sourceTree = "<group>"; };
514C34DE276CE20700FCAB79 /* ComposeMessageService+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeMessageService+State.swift"; sourceTree = "<group>"; };
5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageUploadDetails.swift; sourceTree = "<group>"; };
5168FB0A274F94D300131072 /* MessageAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageAttachment.swift; sourceTree = "<group>"; };
51775C31270B01C200D7C944 /* PrvKeyInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrvKeyInfoTests.swift; sourceTree = "<group>"; };
51775C38270C7D2400D7C944 /* StorageMethod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageMethod.swift; sourceTree = "<group>"; };
517C2E2F2779F90700FECF32 /* MultipartDataRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipartDataRequest.swift; sourceTree = "<group>"; };
517C2E32277A0C6300FECF32 /* EnterpriseServerApiError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnterpriseServerApiError.swift; sourceTree = "<group>"; };
5180CB9027356D48001FC7EF /* MessageSubjectNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSubjectNode.swift; sourceTree = "<group>"; };
5180CB9227357B67001FC7EF /* RawClientConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawClientConfiguration.swift; sourceTree = "<group>"; };
5180CB9427357BB0001FC7EF /* WkdApi.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WkdApi.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1015,6 +1021,16 @@
path = SignIn;
sourceTree = "<group>";
};
517C2E312779F91300FECF32 /* Models */ = {
isa = PBXGroup;
children = (
517C2E32277A0C6300FECF32 /* EnterpriseServerApiError.swift */,
5152F195277E5AED00BE8A5B /* MessageUploadDetails.swift */,
517C2E2F2779F90700FECF32 /* MultipartDataRequest.swift */,
);
path = Models;
sourceTree = "<group>";
};
51B0C772272AB5FD00124663 /* Extensions */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1629,6 +1645,7 @@
children = (
32DCAC9C0512037018F434A1 /* BackendApi.swift */,
21C7DF08266C0D8F00C44800 /* EnterpriseServerApi.swift */,
517C2E312779F91300FECF32 /* Models */,
);
path = "Account Server Services";
sourceTree = "<group>";
Expand Down Expand Up @@ -2573,6 +2590,7 @@
D2E26F6C24F25B1F00612AF1 /* KeyAlgo.swift in Sources */,
D2891AC424C62446008918E3 /* ErrorHandler.swift in Sources */,
51938DC1274CC291007AD57B /* MessageQuoteType.swift in Sources */,
5152F196277E5AED00BE8A5B /* MessageUploadDetails.swift in Sources */,
9F41FA2F253B7624003B970D /* BackupSelectKeyDecorator.swift in Sources */,
D227C0E8250538A90070F805 /* FoldersService.swift in Sources */,
5ADEDCB623A426E300EC495E /* KeyDetailViewController.swift in Sources */,
Expand Down Expand Up @@ -2692,6 +2710,7 @@
9FF0671025520D7100FCC9E6 /* MessageGateway.swift in Sources */,
9F31AB8C23298B3F00CF87EA /* Imap+retry.swift in Sources */,
51B4AE51271444580001F33B /* PubKeyRealmObject.swift in Sources */,
517C2E302779F90700FECF32 /* MultipartDataRequest.swift in Sources */,
9F82D352256D74FA0069A702 /* InboxViewContainerController.swift in Sources */,
D227C0E3250538100070F805 /* LocalFoldersProvider.swift in Sources */,
9FA405C7265AEBA50084D133 /* SetupGenerateKeyViewController.swift in Sources */,
Expand All @@ -2700,6 +2719,7 @@
32DCAF9DA9EC47798DF8BB73 /* SignInViewController.swift in Sources */,
606FE33A2745AA2E009DA039 /* AttachmentViewController.swift in Sources */,
21489B78267CB42400BDE4AC /* LocalClientConfiguration.swift in Sources */,
517C2E33277A0C6300FECF32 /* EnterpriseServerApiError.swift in Sources */,
9FF0671C25520D9D00FCC9E6 /* MailProvider.swift in Sources */,
9FE743072347AA54005E2DBB /* MainNavigationController.swift in Sources */,
9F5C2A8B257E6C4900DE9B4B /* ImapError.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,8 @@ import FlowCryptCommon
protocol EnterpriseServerApiType {
func getActiveFesUrl(for email: String) async throws -> String?
func getClientConfiguration(for email: String) async throws -> RawClientConfiguration
}

enum EnterpriseServerApiError: Error {
case parse
case emailFormat
}

extension EnterpriseServerApiError: LocalizedError {
var errorDescription: String? {
switch self {
case .parse: return "organisational_rules_parse_error_description".localized
case .emailFormat: return "organisational_rules_email_format_error_description".localized
}
}
func getReplyToken(for email: String) async throws -> String
func upload(message: Data, details: MessageUploadDetails) async throws -> String
}

/// server run by individual enterprise customers, serves client configuration
Expand All @@ -37,7 +25,7 @@ class EnterpriseServerApi: EnterpriseServerApiType {
private enum Constants {
/// 404 - Not Found
static let getToleratedHTTPStatuses = [404]
/// -1001 - request timed out, -1003 - сannot resolve host, -1004 - can't conenct to hosts,
/// -1001 - request timed out, -1003 - сannot resolve host, -1004 - can't connect to hosts,
/// -1005 - network connection lost, -1006 - dns lookup failed, -1007 - too many redirects
/// -1008 - resource unavailable
static let getToleratedNSErrorCodes = [-1001, -1003, -1004, -1005, -1006, -1007, -1008]
Expand All @@ -53,6 +41,20 @@ class EnterpriseServerApi: EnterpriseServerApiType {
let clientConfiguration: RawClientConfiguration
}

private struct MessageReplyTokenResponse: Decodable {
let replyToken: String
}

private struct MessageUploadResponse: Decodable {
let url: String
}

private lazy var decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return decoder
}()

private func constructUrlBase(emailDomain: String) -> String {
guard !CommandLine.isDebugBundleWithArgument("--mock-fes-api") else {
return "http://127.0.0.1:8001/fes" // mock
Expand Down Expand Up @@ -99,27 +101,65 @@ class EnterpriseServerApi: EnterpriseServerApiType {
throw EnterpriseServerApiError.emailFormat
}

guard let fesUrl = try await getActiveFesUrl(for: email) else {
return .empty
}
let response: ClientConfigurationResponse = try await performRequest(
email: email,
url: "/api/v1/client-configuration?domain=\(userDomain)",
method: .get,
withAuthorization: false
)

let request = ApiCall.Request(
apiName: Constants.apiName,
url: "\(fesUrl)/api/v1/client-configuration?domain=\(userDomain)"
return response.clientConfiguration
}

func getReplyToken(for email: String) async throws -> String {
let response: MessageReplyTokenResponse = try await performRequest(
email: email,
url: "/api/v1/message/new-reply-token"
)
let safeReponse = try await ApiCall.call(request)

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
return response.replyToken
}

guard let clientConfiguration = (try? decoder.decode(
ClientConfigurationResponse.self,
from: safeReponse.data
))?.clientConfiguration
else {
throw EnterpriseServerApiError.parse
}
return clientConfiguration
func upload(message: Data, details: MessageUploadDetails) async throws -> String {
let detailsData = try details.toJsonData()

let detailsDataItem = MultipartDataItem(
data: detailsData,
name: "details",
contentType: "application/json"
)
let contentDataItem = MultipartDataItem(
data: message,
name: "content",
contentType: "application/octet-stream"
)

let request = MultipartDataRequest(items: [detailsDataItem, contentDataItem])

let contentTypeHeader = URLHeader(
value: "multipart/form-data; boundary=\(request.boundary)",
httpHeaderField: "Content-Type"
)

let response: MessageUploadResponse = try await performRequest(
email: details.from,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a subtle thing: from and current account may not be the same thing in the future - once we add support for different sendAs aliases, in which case you may send an email from alias@example.com while on account account@example.com. So this would really need an account and from separately, or in the future maybe idToken and from separately. For now though, it's ok. I'll file an issue.

url: "/api/v1/message",
headers: [contentTypeHeader],
method: .post,
body: request.httpBody as Data
)

return response.url
}

// MARK: - Helpers
private func getIdToken(email: String) async throws -> String {
let googleService = GoogleUserService(
currentUserEmail: email,
appDelegateGoogleSessionContainer: nil
)

return try await googleService.getCachedOrRefreshedIdToken()
}

private func shouldTolerateWhenCallingOpportunistically(_ error: Error) -> Bool {
Expand All @@ -132,4 +172,39 @@ class EnterpriseServerApi: EnterpriseServerApiType {
}
return true
}

private func performRequest<T: Decodable>(
email: String,
url: String,
headers: [URLHeader] = [],
method: HTTPMethod = .post,
body: Data? = nil,
withAuthorization: Bool = true
) async throws -> T {
guard let fesUrl = try await getActiveFesUrl(for: email) else {
throw EnterpriseServerApiError.noActiveFesUrl
}

if withAuthorization {
let idToken = try await getIdToken(email: email)
let authorizationHeader = URLHeader(value: "Bearer \(idToken)", httpHeaderField: "Authorization")
var headers = headers
headers.append(authorizationHeader)
}

let request = ApiCall.Request(
apiName: Constants.apiName,
url: "\(fesUrl)\(url)",
method: method,
body: body,
headers: headers
)

let safeResponse = try await ApiCall.call(request)

guard let data = try? decoder.decode(T.self, from: safeResponse.data)
else { throw EnterpriseServerApiError.parse }

return data
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// EnterpriseServerApiError.swift
// FlowCrypt
//
// Created by Roma Sosnovsky on 27/12/21
// Copyright © 2017-present FlowCrypt a. s. All rights reserved.
//

import Foundation

enum EnterpriseServerApiError: Error {
case parse
case emailFormat
case noActiveFesUrl
}

extension EnterpriseServerApiError: LocalizedError {
var errorDescription: String? {
switch self {
case .parse: return "organisational_rules_parse_error_description".localized
case .emailFormat: return "organisational_rules_email_format_error_description".localized
case .noActiveFesUrl: return "organisational_rules_fes_url_error_description".localized
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// MessageUploadDetails.swift
// FlowCrypt
//
// Created by Roma Sosnovsky on 30/12/21
// Copyright © 2017-present FlowCrypt a. s. All rights reserved.
//

import Foundation

struct MessageUploadDetails: Encodable {
let associateReplyToken: String
let from: String
let to: [String]
let cc: [String]
let bcc: [String]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// MultipartDataRequest.swift
// FlowCrypt
//
// Created by Roma Sosnovsky on 27/12/21
// Copyright © 2017-present FlowCrypt a. s. All rights reserved.
//

import Foundation

struct MultipartDataRequest {
private let lineSeparator = "\r\n"

let boundary = UUID().uuidString
let httpBody = NSMutableData()

init(items: [MultipartDataItem]) {
items.forEach(append)
appendBoundary()
}

private func append(item: MultipartDataItem) {
httpBody.append("--\(boundary)\(lineSeparator)")
httpBody.append("Content-Disposition: form-data; name=\"\(item.name)\"; filename=\"\(item.name)\"\(lineSeparator)")
httpBody.append("Content-Type: \(item.contentType)\(lineSeparator)\(lineSeparator)")
httpBody.append(item.data)
httpBody.append(lineSeparator)
}

private func appendBoundary() {
httpBody.append("--\(boundary)--\(lineSeparator)")
}
}

struct MultipartDataItem {
let data: Data
let name: String
let contentType: String
}
2 changes: 1 addition & 1 deletion FlowCrypt/Functionality/Services/ApiCall.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extension ApiCall {
struct Request {
var apiName: String
var url: String
var method: HTTPMetod = .get
var method: HTTPMethod = .get
var body: Data?
var headers: [URLHeader] = []
var timeout: TimeInterval = 60.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ extension AttesterApi {

@discardableResult
func update(email: String, pubkey: String, token: String?) async throws -> String {
let httpMethod: HTTPMetod
let httpMethod: HTTPMethod
let headers: [URLHeader]
if let value = token {
httpMethod = .post
Expand Down
1 change: 1 addition & 0 deletions FlowCrypt/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@
// Organisational rules error
"organisational_rules_parse_error_description" = "Couldn't parse data while getting organisational rules";
"organisational_rules_email_format_error_description" = "Wrong user email format";
"organisational_rules_fes_url_error_description" = "Couldn't get active FES url";

"organisational_rules_url_not_valid" = "Please check if key manager url set correctly";
"organisational_rules_autoimport_or_autogen_with_private_key_manager_error" = "Combination of rules (key_manager_url set but PRV_AUTOIMPORT_OR_AUTOGEN is missing) is not supported on this platform";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,12 @@ final class EnterpriseServerApiMock: EnterpriseServerApiType {
getClientConfigurationForCurrentUserCount += 1
return try getClientConfigurationForCurrentUserCall()
}

func getReplyToken(for email: String) async throws -> String {
return ""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better mock value would be mock-reply-token

}

func upload(message: Data, details: MessageUploadDetails) async throws -> String {
return ""
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better mock-web-portal-message-url

}
}
7 changes: 7 additions & 0 deletions FlowCryptCommon/Extensions/Data/DataExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ public extension Data {
}
}

public extension NSMutableData {
func append(_ string: String) {
guard let data = string.data(using: .utf8) else { return }
self.append(data)
}
}

extension String {
init(data: Data) {
self = String(decoding: data, as: UTF8.self)
Expand Down
Loading