add nio connection layer: tls bootstrap, response handler, command runner
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import NIO
|
||||
import NIOIMAPCore
|
||||
import NIOIMAP
|
||||
|
||||
// NIO handlers are confined to their event loop; @unchecked Sendable is the
|
||||
// standard pattern for crossing the actor/event-loop boundary.
|
||||
final class IMAPResponseHandler: ChannelInboundHandler, @unchecked Sendable {
|
||||
typealias InboundIn = Response
|
||||
|
||||
private var buffer: [Response] = []
|
||||
private var expectedTag: String?
|
||||
private var continuation: CheckedContinuation<[Response], Error>?
|
||||
private var greetingContinuation: CheckedContinuation<Void, Error>?
|
||||
|
||||
func channelRead(context: ChannelHandlerContext, data: NIOAny) {
|
||||
let response = unwrapInboundIn(data)
|
||||
buffer.append(response)
|
||||
|
||||
switch response {
|
||||
case .untagged(let payload):
|
||||
if case .conditionalState(let status) = payload, greetingContinuation != nil {
|
||||
switch status {
|
||||
case .ok:
|
||||
greetingContinuation?.resume()
|
||||
greetingContinuation = nil
|
||||
case .preauth:
|
||||
greetingContinuation?.resume()
|
||||
greetingContinuation = nil
|
||||
case .bye(let text):
|
||||
let error = IMAPError.serverError("BYE: \(text)")
|
||||
greetingContinuation?.resume(throwing: error)
|
||||
greetingContinuation = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
case .tagged(let tagged):
|
||||
if tagged.tag == expectedTag {
|
||||
let collected = buffer
|
||||
buffer = []
|
||||
expectedTag = nil
|
||||
continuation?.resume(returning: collected)
|
||||
continuation = nil
|
||||
}
|
||||
case .fatal(let text):
|
||||
let error = IMAPError.serverError("FATAL: \(text)")
|
||||
continuation?.resume(throwing: error)
|
||||
continuation = nil
|
||||
greetingContinuation?.resume(throwing: error)
|
||||
greetingContinuation = nil
|
||||
case .fetch, .authenticationChallenge, .idleStarted:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func errorCaught(context: ChannelHandlerContext, error: Error) {
|
||||
continuation?.resume(throwing: error)
|
||||
continuation = nil
|
||||
greetingContinuation?.resume(throwing: error)
|
||||
greetingContinuation = nil
|
||||
context.close(promise: nil)
|
||||
}
|
||||
|
||||
func waitForGreeting() async throws {
|
||||
try await withCheckedThrowingContinuation { cont in
|
||||
greetingContinuation = cont
|
||||
}
|
||||
}
|
||||
|
||||
func sendCommand(tag: String, continuation cont: CheckedContinuation<[Response], Error>) {
|
||||
expectedTag = tag
|
||||
continuation = cont
|
||||
buffer = []
|
||||
}
|
||||
}
|
||||
|
||||
public enum IMAPError: Error, Sendable {
|
||||
case notConnected
|
||||
case serverError(String)
|
||||
case authenticationFailed
|
||||
case unexpectedResponse(String)
|
||||
}
|
||||
Reference in New Issue
Block a user