add SMTPClient module: connection layer, message formatter, public API, tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user