Files
MagnumOpus/Packages/MagnumOpusCore/Sources/IMAPClient/IMAPResponseHandler.swift
2026-03-14 13:38:39 +01:00

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