diff --git a/Sources/SwiftSMTP/AuthMethod.swift b/Sources/SwiftSMTP/AuthMethod.swift index acd3346..7145157 100644 --- a/Sources/SwiftSMTP/AuthMethod.swift +++ b/Sources/SwiftSMTP/AuthMethod.swift @@ -26,4 +26,6 @@ public enum AuthMethod: String { case plain = "PLAIN" /// XOAUTH2 authentication. Requires a valid access token. case xoauth2 = "XOAUTH2" + /// No authentication at all + case none = "NONE" } diff --git a/Sources/SwiftSMTP/Command.swift b/Sources/SwiftSMTP/Command.swift index 0952cd2..eb9b5cb 100644 --- a/Sources/SwiftSMTP/Command.swift +++ b/Sources/SwiftSMTP/Command.swift @@ -62,6 +62,7 @@ enum Command { case .login: return [.containingChallenge] case .plain: return [.authSucceeded] case .xoauth2: return [.authSucceeded] + case .none: return [.commandOK] } case .authUser: return [.containingChallenge] case .authPassword: return [.authSucceeded] diff --git a/Sources/SwiftSMTP/SMTP.swift b/Sources/SwiftSMTP/SMTP.swift index 466fe1a..81d181c 100644 --- a/Sources/SwiftSMTP/SMTP.swift +++ b/Sources/SwiftSMTP/SMTP.swift @@ -93,6 +93,39 @@ public struct SMTP { self.timeout = timeout } + /// Initializes an `SMTP` instance, specifically for use when communicating with an SMTP + /// host which does not perform authentication. + /// + /// - Parameters: + /// - hostname: Hostname of the SMTP server to connect to, i.e. `smtp.example.com`. + /// - port: Port to connect to the server on. Defaults to `465`. + /// - tlsMode: TLSMode `enum` indicating what form of connection security to use. + /// - tlsConfiguration: `TLSConfiguration` used to connect with TLS. If nil, a configuration with no backing + /// certificates is used. See `TLSConfiguration` for other configuration options. + /// - domainName: Client domain name used when communicating with the server. Defaults to `localhost`. + /// - timeout: How long to try connecting to the server to before returning an error. Defaults to `10` seconds. + /// + /// - Note: + /// - You may need to enable access for less secure apps for your account on the SMTP server. + /// - Some servers like Gmail support IPv6, and if your network does not, you will first attempt to connect via + /// IPv6, then timeout, and fall back to IPv4. You can avoid this by disabling IPv6 on your machine. + public init(hostname: String, + port: Int32 = 587, + tlsMode: TLSMode = .requireSTARTTLS, + tlsConfiguration: TLSConfiguration? = nil, + domainName: String = "localhost", + timeout: UInt = 10) { + self.hostname = hostname + self.email = "" + self.password = "" + self.port = port + self.tlsMode = tlsMode + self.tlsConfiguration = tlsConfiguration + self.authMethods = [String: AuthMethod]() + self.domainName = domainName + self.timeout = timeout + } + /// Send an email. /// /// - Parameters: diff --git a/Sources/SwiftSMTP/SMTPSocket.swift b/Sources/SwiftSMTP/SMTPSocket.swift index ffbbacb..7fd5cab 100644 --- a/Sources/SwiftSMTP/SMTPSocket.swift +++ b/Sources/SwiftSMTP/SMTPSocket.swift @@ -31,6 +31,40 @@ struct SMTPSocket { domainName: String, timeout: UInt) throws { socket = try Socket.create() + let serverOptions = try setupSocket(hostname: hostname, + port: port, + tlsMode: tlsMode, + tlsConfiguration: tlsConfiguration, + domainName: domainName, + timeout: timeout) + let authMethod = try getAuthMethod(authMethods: authMethods, serverOptions: serverOptions, hostname: hostname) + try login(authMethod: authMethod, email: email, password: password) + } + + + /// Initializer for an SMTPSocket when you want to connect to a server that does not + /// require authentication to send messages. + init(hostname: String, + port: Int32, + tlsMode: SMTP.TLSMode, + tlsConfiguration: TLSConfiguration?, + domainName: String, + timeout: UInt) throws { + socket = try Socket.create() + _ = try setupSocket(hostname: hostname, + port: port, + tlsMode: tlsMode, + tlsConfiguration: tlsConfiguration, + domainName: domainName, + timeout: timeout) + } + + private func setupSocket(hostname: String, + port: Int32, + tlsMode: SMTP.TLSMode, + tlsConfiguration: TLSConfiguration?, + domainName: String, + timeout: UInt) throws -> [Response] { if tlsMode == .requireTLS { if let tlsConfiguration = tlsConfiguration { socket.delegate = try tlsConfiguration.makeSSLService() @@ -48,8 +82,7 @@ struct SMTPSocket { throw SMTPError.requiredSTARTTLS } } - let authMethod = try getAuthMethod(authMethods: authMethods, serverOptions: serverOptions, hostname: hostname) - try login(authMethod: authMethod, email: email, password: password) + return serverOptions } func write(_ text: String) throws { @@ -148,9 +181,11 @@ private extension SMTPSocket { } func getAuthMethod(authMethods: [String: AuthMethod], serverOptions: [Response], hostname: String) throws -> AuthMethod { + var requiresAuth = false for option in serverOptions { let components = option.message.components(separatedBy: " ") if components.first == "AUTH" { + requiresAuth = true let _authMethods = components.dropFirst() for authMethod in _authMethods { if let matchingAuthMethod = authMethods[authMethod] { @@ -159,7 +194,13 @@ private extension SMTPSocket { } } } - throw SMTPError.noAuthMethodsOrRequiresTLS(hostname: hostname) + if requiresAuth { + // the server supports AUTH, but no matching methods were found + throw SMTPError.noAuthMethodsOrRequiresTLS(hostname: hostname) + } else { + // the server does not want to hear about AUTH. It's an open relay. + return .none + } } func doStarttls(serverOptions: [Response], tlsConfiguration: TLSConfiguration?) throws -> Bool { @@ -194,6 +235,9 @@ private extension SMTPSocket { try loginPlain(email: email, password: password) case .xoauth2: try loginXOAuth2(email: email, accessToken: password) + case .none: + // don't do anything + return } } diff --git a/Tests/SwiftSMTPTests/Constant.swift b/Tests/SwiftSMTPTests/Constant.swift index d98a427..2d99bf1 100644 --- a/Tests/SwiftSMTPTests/Constant.swift +++ b/Tests/SwiftSMTPTests/Constant.swift @@ -25,6 +25,9 @@ let testDuration: Double = 15 // 📧📧📧 Fill in your own SMTP login info for local testing // ⚠️⚠️⚠️ DO NOT CHECK IN YOUR EMAIL CREDENTALS!!! +let noAuthHost: String? = "localhost" +let noAuthPort: Int32 = 1081 + let hostname = "mail.kitura.dev" let myEmail: String? = nil let myPassword: String? = nil diff --git a/Tests/SwiftSMTPTests/TestMailSender.swift b/Tests/SwiftSMTPTests/TestMailSender.swift index ffed523..9d15efa 100644 --- a/Tests/SwiftSMTPTests/TestMailSender.swift +++ b/Tests/SwiftSMTPTests/TestMailSender.swift @@ -57,6 +57,23 @@ class TestMailSender: XCTestCase { x.fulfill() } } + + func testSendMailNoAuth() throws { + let x = expectation(description: #function) + defer { waitForExpectations(timeout: testDuration) } + + let mail = Mail(from: from, to: [to], subject: #function, text: text) + if let theHost = noAuthHost { + let noAuthSMTP = SMTP(hostname: theHost, port: noAuthPort, tlsMode: .ignoreTLS, + tlsConfiguration: .none) + noAuthSMTP.send(mail) { (err) in + XCTAssertNil(err, String(describing: err)) + x.fulfill() + } + } else { + throw XCTSkip("No no-auth SMTP server configured") + } + } func testSendMailInArray() { let x = expectation(description: #function) diff --git a/Tests/SwiftSMTPTests/TestSMTPSocket.swift b/Tests/SwiftSMTPTests/TestSMTPSocket.swift index 961c1cd..a1aa7da 100644 --- a/Tests/SwiftSMTPTests/TestSMTPSocket.swift +++ b/Tests/SwiftSMTPTests/TestSMTPSocket.swift @@ -19,6 +19,7 @@ import XCTest class TestSMTPSocket: XCTestCase { static var allTests = [ + ("testNoAuth", testNoAuth), ("testBadCredentials", testBadCredentials), ("testBadPort", testBadPort), ("testLogin", testLogin), @@ -27,6 +28,30 @@ class TestSMTPSocket: XCTestCase { ("testSSL", testSSL) ] + func testNoAuth() throws { + if let noAuthHost = noAuthHost { + let x = expectation(description: #function) + defer { waitForExpectations(timeout: testDuration) } + + do { + _ = try SMTPSocket( + hostname: noAuthHost, + port: noAuthPort, + tlsMode: .ignoreTLS, + tlsConfiguration: nil, + domainName: domainName, + timeout: timeout + ) + x.fulfill() + } catch { + XCTFail(String(describing: error)) + x.fulfill() + } + } else { + throw XCTSkip("No no-auth SMTP server configured") + } + } + func testBadCredentials() throws { let x = expectation(description: #function) defer { waitForExpectations(timeout: testDuration) }