Files
MagnumOpus/Packages/MagnumOpusCore/Sources/SMTPClient/SMTPResponseHandler.swift
T
2026-03-14 05:25:03 +01:00

93 lines
2.5 KiB
Swift

import NIO
/// NIO handler that parses SMTP line-based responses.
/// SMTP response format: `<3-digit code><separator><text>\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<SMTPResponse, Error>?
private var greetingContinuation: CheckedContinuation<SMTPResponse, Error>?
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..<lineEnd.lowerBound])
stringBuffer.removeSubrange(stringBuffer.startIndex..<lineEnd.upperBound)
processLine(line)
}
}
private func processLine(_ line: String) {
guard line.count >= 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
}
}
}