83 lines
2.3 KiB
Swift
83 lines
2.3 KiB
Swift
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, RemovableChannelHandler, @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)
|
|
}
|