import Foundation import LilithLogging import MacSyncShared private let log = AppLogger.logger(for: "IMail.Sender") // MARK: - Wire types public struct MailSendPayload: Sendable, Decodable, Equatable { public let to: String public let cc: [String]? public let bcc: [String]? public let subject: String public let body: String public let isHtml: Bool? public init( to: String, cc: [String]? = nil, bcc: [String]? = nil, subject: String, body: String, isHtml: Bool? = nil ) { self.to = to self.cc = cc self.bcc = bcc self.subject = subject self.body = body self.isHtml = isHtml } } public struct PendingMailSend: Sendable, Decodable, Equatable { public let id: String public let action: String public let payload: MailSendPayload public let createdAt: String public init(id: String, action: String, payload: MailSendPayload, createdAt: String) { self.id = id self.action = action self.payload = payload self.createdAt = createdAt } } // MARK: - Applier protocol (hermetic-test seam) /// Narrow protocol the `MailSender` calls into so tests can substitute a /// fake that does not require Mail.app / AppleScript automation access. @MainActor public protocol MailSendApplying: Sendable { func send(_ payload: MailSendPayload) async -> SendQueueApplyResult } // MARK: - Sender /// Default `MailSendApplying` backed by the existing AppleScript-based /// `Sender` (Mail.app driver). Adapts the structured `MailSendPayload` to /// `Sender.SendRequest` and dispatches the synchronous AppleScript call off /// the main thread. @MainActor public final class MailSender: MailSendApplying { private let sender: Sender public init(sender: Sender = .shared) { self.sender = sender } public func send(_ payload: MailSendPayload) async -> SendQueueApplyResult { let request = Sender.SendRequest( to: [payload.to], cc: payload.cc ?? [], bcc: payload.bcc ?? [], subject: payload.subject, body: payload.body, isHTML: payload.isHtml ?? false ) // `Sender.send` runs a blocking AppleScript call — dispatch off main. let result = await Task.detached(priority: .userInitiated) { [sender] in sender.send(request) }.value if result.success { return .sent } else { let reason = result.error ?? "unknown send failure" log.warning("MailSender.send failed: \(reason)") return .failed(reason: reason) } } } // MARK: - SendQueueTransport adapter public struct IMailSendTransport: SendQueueTransport { public typealias PendingItem = PendingMailSend private let apiClient: any IMailAPIClientProtocol public init(apiClient: any IMailAPIClientProtocol) { self.apiClient = apiClient } public func id(of item: PendingMailSend) -> String { item.id } public func fetchPending() async throws -> [PendingMailSend] { guard apiClient.isAuthenticated else { return [] } return try await apiClient.getPendingSends() } public func reportResult(id: String, status: String, error: String?) async throws { try await apiClient.reportSendResult(id: id, status: status, error: error) } }