add IMAPIdleHandler: NIO channel handler for IDLE event streaming

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-14 13:37:16 +01:00
parent 858cdf5284
commit 3ea88f6402

View File

@@ -0,0 +1,72 @@
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, @unchecked Sendable {
typealias InboundIn = Response
private let continuation: AsyncStream<IMAPIdleEvent>.Continuation
private var idleTag: String?
init(continuation: AsyncStream<IMAPIdleEvent>.Continuation) {
self.continuation = continuation
}
func setIdleTag(_ tag: String) {
idleTag = tag
}
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()
}
}