import NIO /// NIO handler that parses SMTP line-based responses. /// SMTP response format: `<3-digit code>\r\n` /// Separator `-` means continuation, space means final line. final class SMTPResponseHandler: ChannelInboundHandler, @unchecked Sendable { typealias InboundIn = ByteBuffer private var stringBuffer = "" private var responseLines: [String] = [] private var responseCode: Int? private var continuation: CheckedContinuation? private var greetingContinuation: CheckedContinuation? func channelRead(context: ChannelHandlerContext, data: NIOAny) { var buffer = unwrapInboundIn(data) guard let received = buffer.readString(length: buffer.readableBytes) else { return } stringBuffer.append(received) while let lineEnd = stringBuffer.range(of: "\r\n") { let line = String(stringBuffer[stringBuffer.startIndex..= 3, let code = Int(line.prefix(3)) else { return } // Extract text after the code + separator character let text: String if line.count > 4 { text = String(line.dropFirst(4)) } else { text = "" } responseLines.append(text) if responseCode == nil { responseCode = code } // Separator is at index 3: `-` for continuation, space for final let isFinal: Bool if line.count > 3 { isFinal = line[line.index(line.startIndex, offsetBy: 3)] == " " } else { isFinal = true } if isFinal { let response = SMTPResponse(code: code, lines: responseLines) responseLines = [] responseCode = nil if let cont = greetingContinuation { greetingContinuation = nil cont.resume(returning: response) } else if let cont = continuation { continuation = nil cont.resume(returning: response) } } } func errorCaught(context: ChannelHandlerContext, error: Error) { if let cont = continuation { continuation = nil cont.resume(throwing: error) } if let cont = greetingContinuation { greetingContinuation = nil cont.resume(throwing: error) } context.close(promise: nil) } func waitForGreeting() async throws -> SMTPResponse { try await withCheckedThrowingContinuation { cont in greetingContinuation = cont } } func waitForResponse() async throws -> SMTPResponse { try await withCheckedThrowingContinuation { cont in continuation = cont } } }