import NIO import NIOIMAPCore import NIOIMAP /// Events emitted by the IDLE handler public enum IMAPIdleEvent: Sendable { case exists(Int) case expunge(Int) case idleTerminated } /// NIO ChannelInboundHandler that processes untagged responses during IMAP IDLE. /// Unlike the standard IMAPResponseHandler (which uses CheckedContinuation for tagged responses), /// this handler uses AsyncStream to deliver a continuous stream of events. final class IMAPIdleHandler: ChannelInboundHandler, RemovableChannelHandler, @unchecked Sendable { typealias InboundIn = Response private let continuation: AsyncStream.Continuation private let idleTag: String init(continuation: AsyncStream.Continuation, idleTag: String) { self.continuation = continuation self.idleTag = idleTag } func channelRead(context: ChannelHandlerContext, data: NIOAny) { let response = unwrapInboundIn(data) switch response { case .untagged(let payload): switch payload { case .mailboxData(let data): switch data { case .exists(let count): continuation.yield(.exists(count)) default: break } case .messageData(let data): switch data { case .expunge(let seqNum): continuation.yield(.expunge(Int(seqNum.rawValue))) default: break } default: break } case .tagged(let tagged): if tagged.tag == idleTag { continuation.yield(.idleTerminated) } case .idleStarted: // Server acknowledged IDLE — we're now idling break case .fetch, .authenticationChallenge, .fatal: break } } func errorCaught(context: ChannelHandlerContext, error: Error) { continuation.finish() context.close(promise: nil) } func channelInactive(context: ChannelHandlerContext) { continuation.finish() } }