import Alamofire import Foundation import LilithAgentCore import LilithLogging import MacSyncShared import SwiftyJSON private let log = AppLogger.logger(for: "IMail.API") // MARK: - Payload Types public struct EmailAddressPayload { public let address: String public let name: String? var dictionary: [String: Any?] { ["address": address, "name": name] } } public struct SyncEmailPayload { public let messageId: String public let threadId: String? public let subject: String? public let fromAddress: String public let fromName: String? public let to: [EmailAddressPayload] public let cc: [EmailAddressPayload] public let folder: String public let direction: String public let textBody: String? public let htmlBody: String? public let hasAttachments: Bool? public let attachmentCount: Int? public let sentAt: String public let receivedAt: String? public let isRead: Bool? var dictionary: [String: Any?] { [ "messageId": messageId, "threadId": threadId, "subject": subject, "fromAddress": fromAddress, "fromName": fromName, "to": to.map { $0.dictionary }, "cc": cc.map { $0.dictionary }, "folder": folder, "direction": direction, "textBody": textBody, "htmlBody": htmlBody, "hasAttachments": hasAttachments, "attachmentCount": attachmentCount, "sentAt": sentAt, "receivedAt": receivedAt, "isRead": isRead, ] } } public struct IMailStatsResponse { public let totalEmails: Int public let totalFolders: Int public let lastSyncAt: Date? } // MARK: - Protocol public protocol IMailAPIClientProtocol: AnyObject, Sendable { var isAuthenticated: Bool { get } func syncMail(_ payloads: [SyncEmailPayload]) async throws -> Int func getStats() async throws -> IMailStatsResponse func getPendingSends() async throws -> [PendingMailSend] func reportSendResult(id: String, status: String, error: String?) async throws } // MARK: - APIClient public final class APIClient: BaseAPIClient, IMailAPIClientProtocol, @unchecked Sendable { public static let shared: APIClient = { APIClient(baseURL: macSyncResolveServerURL(), keychain: macSyncSharedKeychain) }() public var isAuthenticated: Bool { (try? getAuthToken()) != nil } // MARK: - Mail Sync public func syncMail(_ payloads: [SyncEmailPayload]) async throws -> Int { let params: [String: Any] = ["emails": payloads.map { $0.dictionary }] let data = try await authenticatedRequest("/client/imail/sync", method: .post, parameters: params) let json = JSON(data) guard json["success"].boolValue else { let msg = json["error"]["message"].stringValue throw APIError.serverError(statusCode: json["statusCode"].intValue, message: msg.isEmpty ? "Server error" : msg) } let synced = json["data"]["synced"].intValue log.info("syncMail success synced=\(synced)") return synced } // MARK: - Send Queue public func getPendingSends() async throws -> [PendingMailSend] { let data = try await authenticatedRequest("/client/imail/send-queue/pending", method: .get) let json = JSON(data) guard json["success"].boolValue else { throw APIError.serverError(statusCode: 0, message: json["error"]["message"].stringValue) } return json["data"]["items"].arrayValue.map { item in let p = item["payload"] let cc = p["cc"].array?.compactMap { $0.string } let bcc = p["bcc"].array?.compactMap { $0.string } return PendingMailSend( id: item["id"].stringValue, action: item["action"].stringValue, payload: MailSendPayload( to: p["to"].stringValue, cc: cc, bcc: bcc, subject: p["subject"].stringValue, body: p["body"].stringValue, isHtml: p["isHtml"].bool ), createdAt: item["createdAt"].stringValue ) } } public func reportSendResult(id: String, status: String, error: String?) async throws { var params: [String: Any] = ["status": status] if let err = error { params["error"] = err } let data = try await authenticatedRequest("/client/imail/send-queue/\(id)/result", method: .post, parameters: params) let json = JSON(data) guard json["success"].boolValue else { throw APIError.serverError(statusCode: 0, message: json["error"]["message"].stringValue) } } // MARK: - Stats public func getStats() async throws -> IMailStatsResponse { let data = try await authenticatedRequest("/client/imail/stats", method: .get) let json = JSON(data) guard json["success"].boolValue else { throw APIError.serverError(statusCode: 0, message: json["error"]["message"].stringValue) } return IMailStatsResponse( totalEmails: json["data"]["totalEmails"].intValue, totalFolders: json["data"]["totalFolders"].intValue, lastSyncAt: json["data"]["lastSyncAt"].string.flatMap { ISO8601DateFormatter().date(from: $0) } ) } }