Extend IMAPClientProtocol with storeFlags, moveMessage, copyMessage, expunge, appendMessage, capabilities methods. Implement all six in IMAPClient actor using NIOIMAPCore typed commands. Add multi-part command support to IMAPConnection/IMAPCommandRunner for APPEND. MockIMAPClient tracks all write calls for testing. SyncCoordinator detects mailbox roles from LIST attributes with name-based fallback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
71 lines
2.2 KiB
Swift
71 lines
2.2 KiB
Swift
import NIO
|
|
import NIOIMAPCore
|
|
@preconcurrency import NIOIMAP
|
|
@preconcurrency import NIOSSL
|
|
|
|
actor IMAPConnection {
|
|
private let host: String
|
|
private let port: Int
|
|
private let group: EventLoopGroup
|
|
private var channel: Channel?
|
|
private let responseHandler: IMAPResponseHandler
|
|
|
|
init(host: String, port: Int) {
|
|
self.host = host
|
|
self.port = port
|
|
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
self.responseHandler = IMAPResponseHandler()
|
|
}
|
|
|
|
func connect() async throws {
|
|
let sslContext = try NIOSSLContext(configuration: TLSConfiguration.makeClientConfiguration())
|
|
let handler = responseHandler
|
|
let hostname = host
|
|
|
|
let bootstrap = ClientBootstrap(group: group)
|
|
.channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
|
|
.channelInitializer { channel in
|
|
let sslHandler = try! NIOSSLClientHandler(context: sslContext, serverHostname: hostname)
|
|
return channel.pipeline.addHandlers([
|
|
sslHandler,
|
|
IMAPClientHandler(),
|
|
handler,
|
|
])
|
|
}
|
|
|
|
channel = try await bootstrap.connect(host: host, port: port).get()
|
|
try await handler.waitForGreeting()
|
|
}
|
|
|
|
func sendCommand(_ tag: String, command: CommandStreamPart) async throws -> [Response] {
|
|
guard let channel else { throw IMAPError.notConnected }
|
|
let handler = responseHandler
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
handler.sendCommand(tag: tag, continuation: continuation)
|
|
channel.writeAndFlush(IMAPClientHandler.Message.part(command), promise: nil)
|
|
}
|
|
}
|
|
|
|
/// Send multiple command parts as a single multi-part command (e.g. APPEND).
|
|
/// The tag must match the tag embedded in the first part.
|
|
func sendMultiPartCommand(_ tag: String, parts: [CommandStreamPart]) async throws -> [Response] {
|
|
guard let channel else { throw IMAPError.notConnected }
|
|
let handler = responseHandler
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
handler.sendCommand(tag: tag, continuation: continuation)
|
|
for part in parts {
|
|
channel.writeAndFlush(IMAPClientHandler.Message.part(part), promise: nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
func disconnect() async throws {
|
|
try await channel?.close()
|
|
channel = nil
|
|
}
|
|
|
|
func shutdown() async throws {
|
|
try await group.shutdownGracefully()
|
|
}
|
|
}
|