From e5ff209d0645414d31a5bdfd0cea38271d9bbeb3 Mon Sep 17 00:00:00 2001 From: Stephen Beitzel Date: Sat, 15 Jan 2022 12:17:14 -0800 Subject: [PATCH 1/3] Allow for connecting to open relays --- Sources/SwiftSMTP/AuthMethod.swift | 2 ++ Sources/SwiftSMTP/Command.swift | 1 + Sources/SwiftSMTP/SMTPSocket.swift | 17 ++++++++++++-- Tests/SwiftSMTPTests/Constant.swift | 5 +++- Tests/SwiftSMTPTests/TestSMTPSocket.swift | 28 +++++++++++++++++++++++ 5 files changed, 50 insertions(+), 3 deletions(-) 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/SMTPSocket.swift b/Sources/SwiftSMTP/SMTPSocket.swift index ffbbacb..5e4a0d7 100644 --- a/Sources/SwiftSMTP/SMTPSocket.swift +++ b/Sources/SwiftSMTP/SMTPSocket.swift @@ -49,7 +49,9 @@ struct SMTPSocket { } } let authMethod = try getAuthMethod(authMethods: authMethods, serverOptions: serverOptions, hostname: hostname) - try login(authMethod: authMethod, email: email, password: password) + if authMethod != .none { + try login(authMethod: authMethod, email: email, password: password) + } } func write(_ text: String) throws { @@ -148,9 +150,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 +163,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 +204,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..d35bf5d 100644 --- a/Tests/SwiftSMTPTests/Constant.swift +++ b/Tests/SwiftSMTPTests/Constant.swift @@ -25,8 +25,11 @@ 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 myEmail: String? = "tester@local" let myPassword: String? = nil let portTLS: Int32 = 465 let portPlain: Int32 = 2525 diff --git a/Tests/SwiftSMTPTests/TestSMTPSocket.swift b/Tests/SwiftSMTPTests/TestSMTPSocket.swift index 961c1cd..0e51788 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,33 @@ 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, + email: email, + password: "don't care", + port: noAuthPort, + tlsMode: .ignoreTLS, + tlsConfiguration: nil, + authMethods: [String: AuthMethod](), + 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) } From 24dab31474b134d017f73e86e73b27792d75fc88 Mon Sep 17 00:00:00 2001 From: Stephen Beitzel Date: Sun, 16 Jan 2022 15:58:46 -0800 Subject: [PATCH 2/3] revert testing email --- Tests/SwiftSMTPTests/Constant.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SwiftSMTPTests/Constant.swift b/Tests/SwiftSMTPTests/Constant.swift index d35bf5d..2d99bf1 100644 --- a/Tests/SwiftSMTPTests/Constant.swift +++ b/Tests/SwiftSMTPTests/Constant.swift @@ -29,7 +29,7 @@ let noAuthHost: String? = "localhost" let noAuthPort: Int32 = 1081 let hostname = "mail.kitura.dev" -let myEmail: String? = "tester@local" +let myEmail: String? = nil let myPassword: String? = nil let portTLS: Int32 = 465 let portPlain: Int32 = 2525 From b764fabfa5a1ece18bb8a245ce5dc5c5632754f4 Mon Sep 17 00:00:00 2001 From: Stephen Beitzel Date: Sat, 22 Jan 2022 11:12:27 -0800 Subject: [PATCH 3/3] Refactor SMTPSocket and SMTP with a specifically no-auth constructor. --- Sources/SwiftSMTP/SMTP.swift | 33 +++++++++++++++++++ Sources/SwiftSMTP/SMTPSocket.swift | 39 ++++++++++++++++++++--- Tests/SwiftSMTPTests/TestMailSender.swift | 17 ++++++++++ Tests/SwiftSMTPTests/TestSMTPSocket.swift | 3 -- 4 files changed, 85 insertions(+), 7 deletions(-) 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 5e4a0d7..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,10 +82,7 @@ struct SMTPSocket { throw SMTPError.requiredSTARTTLS } } - let authMethod = try getAuthMethod(authMethods: authMethods, serverOptions: serverOptions, hostname: hostname) - if authMethod != .none { - try login(authMethod: authMethod, email: email, password: password) - } + return serverOptions } func write(_ text: String) throws { 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 0e51788..a1aa7da 100644 --- a/Tests/SwiftSMTPTests/TestSMTPSocket.swift +++ b/Tests/SwiftSMTPTests/TestSMTPSocket.swift @@ -36,12 +36,9 @@ class TestSMTPSocket: XCTestCase { do { _ = try SMTPSocket( hostname: noAuthHost, - email: email, - password: "don't care", port: noAuthPort, tlsMode: .ignoreTLS, tlsConfiguration: nil, - authMethods: [String: AuthMethod](), domainName: domainName, timeout: timeout )