import Foundation import NIO @preconcurrency import NIOSSL import Models actor SMTPConnection { private let host: String private let port: Int private let security: SMTPSecurity private let group: EventLoopGroup private var channel: Channel? private let responseHandler: SMTPResponseHandler init(host: String, port: Int, security: SMTPSecurity) { self.host = host self.port = port self.security = security self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1) self.responseHandler = SMTPResponseHandler() } func connect() async throws -> SMTPResponse { let handler = responseHandler let hostname = host let bootstrap: ClientBootstrap if security == .ssl { let sslContext = try NIOSSLContext(configuration: TLSConfiguration.makeClientConfiguration()) bootstrap = ClientBootstrap(group: group) .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) .channelInitializer { channel in let sslHandler = try! NIOSSLClientHandler(context: sslContext, serverHostname: hostname) return channel.pipeline.addHandlers([sslHandler, handler]) } } else { // STARTTLS: start plain, upgrade later bootstrap = ClientBootstrap(group: group) .channelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1) .channelInitializer { channel in channel.pipeline.addHandler(handler) } } do { channel = try await bootstrap.connect(host: host, port: port).get() } catch { throw SMTPError.connectionFailed(error.localizedDescription) } return try await handler.waitForGreeting() } func sendCommand(_ command: String) async throws -> SMTPResponse { guard let channel else { throw SMTPError.notConnected } let handler = responseHandler var buffer = channel.allocator.buffer(capacity: command.utf8.count + 2) buffer.writeString(command + "\r\n") channel.writeAndFlush(buffer, promise: nil) return try await handler.waitForResponse() } func upgradeToTLS() async throws { guard let channel else { throw SMTPError.notConnected } let sslContext = try NIOSSLContext(configuration: TLSConfiguration.makeClientConfiguration()) let sslHandler = try NIOSSLClientHandler(context: sslContext, serverHostname: host) do { try await channel.pipeline.addHandler(sslHandler, position: .first).get() } catch { throw SMTPError.tlsUpgradeFailed } } func sendRawBytes(_ data: Data) async throws { guard let channel else { throw SMTPError.notConnected } var buffer = channel.allocator.buffer(capacity: data.count) buffer.writeBytes(data) channel.writeAndFlush(buffer, promise: nil) } func disconnect() async throws { try await channel?.close() channel = nil } func shutdown() async throws { try await group.shutdownGracefully() } }