macsync/@packages/imail/Sources/IMailSync/SendQueueAdapter.swift

117 lines
3.4 KiB
Swift
Raw Normal View History

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)
}
}