import Foundation import Models /// Public SMTP client for sending email messages. public actor SMTPClient { private let host: String private let port: Int private let security: SMTPSecurity private let credentials: Credentials public init(host: String, port: Int, security: SMTPSecurity, credentials: Credentials) { self.host = host self.port = port self.security = security self.credentials = credentials } /// Send an outgoing email message through a full SMTP session. /// Performs: connect → EHLO → [STARTTLS + re-EHLO] → AUTH → MAIL FROM → RCPT TO → DATA → QUIT public func send(message: OutgoingMessage) async throws { let connection = SMTPConnection(host: host, port: port, security: security) let runner = SMTPCommandRunner(connection: connection) let domain = MessageFormatter.domainFromEmail(message.from.address) defer { Task { try? await runner.quit() try? await connection.disconnect() try? await connection.shutdown() } } _ = try await connection.connect() _ = try await runner.ehlo(hostname: domain) if security == .starttls { try await runner.startTLS() _ = try await runner.ehlo(hostname: domain) } try await runner.authenticate(credentials: credentials) try await runner.mailFrom(message.from.address) // All recipients: to + cc + bcc let allRecipients = message.to + message.cc + message.bcc for recipient in allRecipients { try await runner.rcptTo(recipient.address) } let formattedMessage = MessageFormatter.format(message) try await runner.data(formattedMessage) } /// Test the SMTP connection by performing auth without sending. /// Performs: connect → EHLO → [STARTTLS + re-EHLO] → AUTH → QUIT public func testConnection() async throws { let connection = SMTPConnection(host: host, port: port, security: security) let runner = SMTPCommandRunner(connection: connection) let domain = MessageFormatter.domainFromEmail(credentials.username) defer { Task { try? await runner.quit() try? await connection.disconnect() try? await connection.shutdown() } } _ = try await connection.connect() _ = try await runner.ehlo(hostname: domain) if security == .starttls { try await runner.startTLS() _ = try await runner.ehlo(hostname: domain) } try await runner.authenticate(credentials: credentials) } }