54ce92e280
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
93 lines
2.5 KiB
Swift
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
|
|
}
|
|
}
|
|
}
|