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