Skip to content

Commit ec3fa12

Browse files
authored
Starttls merge (#81)
* Adds STARTTLS support * Adds TLSMode enum to support various tls and unencrypted modes. Change default SMTP init to require STARTTLS and port 587 * Improve comments and names * fix tests * fix tests, update readme/migration, regen jazzy * add tests to LinuxMain
1 parent 54fbbb9 commit ec3fa12

38 files changed

Lines changed: 968 additions & 86 deletions

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ macOS & Linux: `Swift 4.0.3` or `Swift 4.1`
2323

2424
## Migration Guide
2525

26-
Version `4.0.0` & `3.0.0` bring breaking changes. See the quick migration guide [here](https://github.com/IBM-Swift/Swift-SMTP/blob/master/migration-guide.md).
26+
Version `5.0.0` brings breaking changes. See the quick migration guide [here](https://github.com/IBM-Swift/Swift-SMTP/blob/master/migration-guide.md).
2727

2828
## Usage
2929

@@ -47,15 +47,15 @@ Additional parameters of `SMTP` struct:
4747
public init(hostname: String,
4848
email: String,
4949
password: String,
50-
port: Int32 = 465,
51-
useTLS: Bool = true,
50+
port: Int32 = 587,
51+
tlsMode: TLSMode = .requireSTARTTLS,
5252
tlsConfiguration: TLSConfiguration? = nil,
5353
authMethods: [AuthMethod] = [],
5454
domainName: String = "localhost",
5555
timeout: UInt = 10)
5656
```
5757

58-
By default, the `SMTP` struct connects on port `465` and tries to connect using TLS. It also uses a `TLSConfiguration` that uses no backing certificates. Configure these to your needs. For more info on `TLSConfiguration`, view the [docs](https://ibm-swift.github.io/Swift-SMTP/Structs/TLSConfiguration.html).
58+
By default, the `SMTP` struct connects on port `587` and sends mail only if a TLS connection can be established. It also uses a `TLSConfiguration` that uses no backing certificates. View the [docs](https://ibm-swift.github.io/Swift-SMTP/) for more configuration options.
5959

6060
### Send email
6161

Sources/SwiftSMTP/SMTP.swift

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,21 +22,35 @@ public struct SMTP {
2222
private let email: String
2323
private let password: String
2424
private let port: Int32
25-
private let useTLS: Bool
25+
private let tlsMode: TLSMode
2626
private let tlsConfiguration: TLSConfiguration?
2727
private let authMethods: [String: AuthMethod]
2828
private let domainName: String
2929
private let timeout: UInt
3030

31+
/// TLSMode enum for what form of connection security to enforce.
32+
public enum TLSMode {
33+
/// Upgrades the connection to TLS if STARTLS command is received, else sends mail without security.
34+
case normal
35+
36+
/// Send mail over plaintext and ignore STARTTLS commands and TLS options. Could throw an error if server requires TLS.
37+
case ignoreTLS
38+
39+
/// Only send mail after an initial successful TLS connection. Connection will fail if a TLS connection cannot be established. The default port, 587, will likely need to be adjusted depending on your server.
40+
case requireTLS
41+
42+
/// Expect a STARTTLS command from the server and require the connection is upgraded to TLS. Will throw if the server does not issue a STARTTLS command.
43+
case requireSTARTTLS
44+
}
45+
3146
/// Initializes an `SMTP` instance.
3247
///
3348
/// - Parameters:
3449
/// - hostname: Hostname of the SMTP server to connect to, i.e. `smtp.example.com`.
3550
/// - email: Username to log in to server.
3651
/// - password: Password to log in to server, or access token if using XOAUTH2 authorization method.
3752
/// - port: Port to connect to the server on. Defaults to `465`.
38-
/// - useTLS: `Bool` indicating whether to connect with TLS. Your server must support the `STARTTLS` command.
39-
/// Defaults to `true`.
53+
/// - tlsMode: TLSMode `enum` indicating what form of connection security to use.
4054
/// - tlsConfiguration: `TLSConfiguration` used to connect with TLS. If nil, a configuration with no backing
4155
/// certificates is used. See `TLSConfiguration` for other configuration options.
4256
/// - authMethods: `AuthMethod`s to use to log in to the server. If blank, tries all supported methods.
@@ -50,8 +64,8 @@ public struct SMTP {
5064
public init(hostname: String,
5165
email: String,
5266
password: String,
53-
port: Int32 = 465,
54-
useTLS: Bool = true,
67+
port: Int32 = 587,
68+
tlsMode: TLSMode = .requireSTARTTLS,
5569
tlsConfiguration: TLSConfiguration? = nil,
5670
authMethods: [AuthMethod] = [],
5771
domainName: String = "localhost",
@@ -60,7 +74,7 @@ public struct SMTP {
6074
self.email = email
6175
self.password = password
6276
self.port = port
63-
self.useTLS = useTLS
77+
self.tlsMode = tlsMode
6478
self.tlsConfiguration = tlsConfiguration
6579

6680
let _authMethods = !authMethods.isEmpty ? authMethods : [
@@ -126,7 +140,7 @@ public struct SMTP {
126140
email: email,
127141
password: password,
128142
port: port,
129-
useTLS: useTLS,
143+
tlsMode: tlsMode,
130144
tlsConfiguration: tlsConfiguration,
131145
authMethods: authMethods,
132146
domainName: domainName,

Sources/SwiftSMTP/SMTPError.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ public enum SMTPError: Error, CustomStringConvertible {
3030
/// File not found at path while trying to send file `Attachment`.
3131
case fileNotFound(path: String)
3232

33-
/// The preferred `AuthMethod`s could not be found. Connecting with `SSL` may be required.
34-
case noSupportedAuthMethods(hostname: String)
33+
/// The preferred `AuthMethod`s could not be found, or your server is sending back a STARTTLS command and requires a connection upgrade.
34+
case noAuthMethodsOrRequiresTLS(hostname: String)
3535

3636
// Sender
3737
/// Mail has no recipients.
@@ -50,19 +50,23 @@ public enum SMTPError: Error, CustomStringConvertible {
5050
// User
5151
/// Invalid email provided for `User`.
5252
case invalidEmail(email: String)
53+
54+
/// STARTTLS was required but the server did not request it.
55+
case requiredSTARTTLS
5356

5457
/// Description of the `SMTPError`.
5558
public var description: String {
5659
switch self {
5760
case .base64DecodeFail(let s): return "Error decoding string: \(s)."
5861
case .md5HashChallengeFail: return "Hashing server challenge with MD5 algorithm failed."
5962
case .fileNotFound(let p): return "File not found at path while trying to send file `Attachment`: \(p)."
60-
case .noSupportedAuthMethods(let hostname): return "The preferred authorization methods could not be found on \(hostname). Connecting with SSL may be required."
63+
case .noAuthMethodsOrRequiresTLS(let hostname): return "The preferred authorization methods could not be found on \(hostname), or your server is sending back a STARTTLS command and requires a connection upgrade."
6164
case .noRecipients: return "An email requires at least one recipient."
6265
case .createEmailRegexFailed: return "Failed to create RegularExpression that can check if an email is valid."
6366
case .badResponse(let command, let response): return "Bad response received for command. command: (\(command)), response: \(response)"
6467
case .convertDataUTF8Fail(let buf): return "Error converting Data read from socket to a String: \(buf)."
6568
case .invalidEmail(let email): return "Invalid email provided for User: \(email)."
69+
case .requiredSTARTTLS: return "STARTTLS was required but the server did not issue a STARTTLS command."
6670
}
6771
}
6872

Sources/SwiftSMTP/SMTPSocket.swift

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ struct SMTPSocket {
2525
email: String,
2626
password: String,
2727
port: Int32,
28-
useTLS: Bool,
28+
tlsMode: SMTP.TLSMode,
2929
tlsConfiguration: TLSConfiguration?,
3030
authMethods: [String: AuthMethod],
3131
domainName: String,
3232
timeout: UInt) throws {
3333
socket = try Socket.create()
34-
if useTLS {
34+
if tlsMode == .requireTLS {
3535
if let tlsConfiguration = tlsConfiguration {
3636
socket.delegate = try tlsConfiguration.makeSSLService()
3737
} else {
@@ -40,7 +40,14 @@ struct SMTPSocket {
4040
}
4141
try socket.connect(to: hostname, port: port, timeout: timeout * 1000)
4242
try parseResponses(readFromSocket(), command: .connect)
43-
let serverOptions = try getServerOptions(domainName: domainName)
43+
var serverOptions = try getServerOptions(domainName: domainName)
44+
if tlsMode == .requireSTARTTLS || tlsMode == .normal {
45+
if try doStarttls(serverOptions: serverOptions, tlsConfiguration: tlsConfiguration) {
46+
serverOptions = try getServerOptions(domainName: domainName)
47+
} else if tlsMode == .requireSTARTTLS {
48+
throw SMTPError.requiredSTARTTLS
49+
}
50+
}
4451
let authMethod = try getAuthMethod(authMethods: authMethods, serverOptions: serverOptions, hostname: hostname)
4552
try login(authMethod: authMethod, email: email, password: password)
4653
}
@@ -152,7 +159,29 @@ private extension SMTPSocket {
152159
}
153160
}
154161
}
155-
throw SMTPError.noSupportedAuthMethods(hostname: hostname)
162+
throw SMTPError.noAuthMethodsOrRequiresTLS(hostname: hostname)
163+
}
164+
165+
func doStarttls(serverOptions: [Response], tlsConfiguration: TLSConfiguration?) throws -> Bool {
166+
for option in serverOptions {
167+
if option.message == "STARTTLS" {
168+
try starttls(tlsConfiguration: tlsConfiguration)
169+
return true
170+
}
171+
}
172+
return false
173+
}
174+
175+
func starttls(tlsConfiguration: TLSConfiguration?) throws {
176+
try send(.starttls)
177+
// Upgrade the socket to SSL/TLS
178+
if let tlsConfiguration = tlsConfiguration {
179+
socket.delegate = try tlsConfiguration.makeSSLService()
180+
} else {
181+
socket.delegate = try TLSConfiguration().makeSSLService()
182+
}
183+
try socket.delegate?.initialize(asServer: false)
184+
try socket.delegate?.onConnect(socket: socket)
156185
}
157186

158187
func login(authMethod: AuthMethod, email: String, password: String) throws {

Tests/LinuxMain.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,5 @@ XCTMain([
5050
testCase(TestDataSender.allTests.shuffled()),
5151
testCase(TestMailSender.allTests.shuffled()),
5252
testCase(TestMiscellaneous.allTests.shuffled()),
53-
testCase(TestSMTPSocket.allTests.shuffled())].shuffled())
53+
testCase(TestSMTPSocket.allTests.shuffled()),
54+
testCase(TestTLSMode.allTests.shuffled())].shuffled())

Tests/SwiftSMTPTests/Constant.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ let testDuration: Double = 15
2828
let hostname = "smtp.gmail.com"
2929
let myEmail: String? = nil
3030
let myPassword: String? = nil
31-
let port: Int32 = 465
32-
let useTLS = true
31+
let port: Int32 = 587
3332
let authMethods: [String: AuthMethod] = [
3433
AuthMethod.cramMD5.rawValue: .cramMD5,
3534
AuthMethod.login.rawValue: .login,

Tests/SwiftSMTPTests/TestSMTPSocket.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class TestSMTPSocket: XCTestCase {
3737
email: email,
3838
password: "bad password",
3939
port: port,
40-
useTLS: useTLS,
40+
tlsMode: .requireSTARTTLS,
4141
tlsConfiguration: nil,
4242
authMethods: authMethods,
4343
domainName: domainName,
@@ -65,7 +65,7 @@ class TestSMTPSocket: XCTestCase {
6565
email: email,
6666
password: password,
6767
port: 1,
68-
useTLS: useTLS,
68+
tlsMode: .requireSTARTTLS,
6969
tlsConfiguration: nil,
7070
authMethods: authMethods,
7171
domainName: domainName,
@@ -88,7 +88,7 @@ class TestSMTPSocket: XCTestCase {
8888
email: email,
8989
password: password,
9090
port: port,
91-
useTLS: useTLS,
91+
tlsMode: .requireSTARTTLS,
9292
tlsConfiguration: nil,
9393
authMethods: [AuthMethod.login.rawValue: .login],
9494
domainName: domainName,
@@ -111,7 +111,7 @@ class TestSMTPSocket: XCTestCase {
111111
email: email,
112112
password: password,
113113
port: port,
114-
useTLS: useTLS,
114+
tlsMode: .requireSTARTTLS,
115115
tlsConfiguration: nil,
116116
authMethods: [AuthMethod.plain.rawValue: .plain],
117117
domainName: domainName,
@@ -134,7 +134,7 @@ class TestSMTPSocket: XCTestCase {
134134
email: email,
135135
password: password,
136136
port: 0,
137-
useTLS: useTLS,
137+
tlsMode: .requireSTARTTLS,
138138
tlsConfiguration: nil,
139139
authMethods: authMethods,
140140
domainName: domainName,
@@ -157,7 +157,7 @@ class TestSMTPSocket: XCTestCase {
157157
email: email,
158158
password: password,
159159
port: port,
160-
useTLS: useTLS,
160+
tlsMode: .requireSTARTTLS,
161161
tlsConfiguration: tlsConfiguration,
162162
authMethods: authMethods,
163163
domainName: domainName,
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* Copyright IBM Corporation 2018
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
**/
16+
17+
import XCTest
18+
@testable import SwiftSMTP
19+
20+
class TestTLSMode: XCTestCase {
21+
static var allTests = [
22+
("testNormal", testNormal),
23+
("testIgnoreTLS", testIgnoreTLS),
24+
("testRequireTLS", testRequireTLS),
25+
("testRequireSTARTTLS", testRequireSTARTTLS)
26+
]
27+
28+
func testNormal() {
29+
let expectation = self.expectation(description: #function)
30+
defer { waitForExpectations(timeout: testDuration) }
31+
32+
do {
33+
_ = try SMTPSocket(
34+
hostname: hostname,
35+
email: email,
36+
password: password,
37+
port: port,
38+
tlsMode: .normal,
39+
tlsConfiguration: nil,
40+
authMethods: authMethods,
41+
domainName: domainName,
42+
timeout: timeout
43+
)
44+
expectation.fulfill()
45+
} catch {
46+
XCTFail(String(describing: error))
47+
expectation.fulfill()
48+
}
49+
}
50+
51+
func testIgnoreTLS() {
52+
let expectation = self.expectation(description: #function)
53+
defer { waitForExpectations(timeout: testDuration) }
54+
55+
do {
56+
_ = try SMTPSocket(
57+
hostname: hostname,
58+
email: email,
59+
password: password,
60+
port: port,
61+
tlsMode: .ignoreTLS,
62+
tlsConfiguration: nil,
63+
authMethods: authMethods,
64+
domainName: domainName,
65+
timeout: timeout
66+
)
67+
XCTFail()
68+
expectation.fulfill()
69+
} catch {
70+
if case SMTPError.noAuthMethodsOrRequiresTLS = error {
71+
expectation.fulfill()
72+
} else {
73+
XCTFail(String(describing: error))
74+
expectation.fulfill()
75+
}
76+
}
77+
}
78+
79+
func testRequireTLS() {
80+
let expectation = self.expectation(description: #function)
81+
defer { waitForExpectations(timeout: testDuration) }
82+
83+
do {
84+
_ = try SMTPSocket(
85+
hostname: hostname,
86+
email: email,
87+
password: password,
88+
port: 465,
89+
tlsMode: .requireTLS,
90+
tlsConfiguration: nil,
91+
authMethods: authMethods,
92+
domainName: domainName,
93+
timeout: timeout
94+
)
95+
expectation.fulfill()
96+
} catch {
97+
XCTFail(String(describing: error))
98+
expectation.fulfill()
99+
}
100+
}
101+
102+
func testRequireSTARTTLS() {
103+
let expectation = self.expectation(description: #function)
104+
defer { waitForExpectations(timeout: testDuration) }
105+
106+
do {
107+
_ = try SMTPSocket(
108+
hostname: hostname,
109+
email: email,
110+
password: password,
111+
port: port,
112+
tlsMode: .requireSTARTTLS,
113+
tlsConfiguration: nil,
114+
authMethods: authMethods,
115+
domainName: domainName,
116+
timeout: timeout
117+
)
118+
expectation.fulfill()
119+
} catch {
120+
XCTFail(String(describing: error))
121+
expectation.fulfill()
122+
}
123+
}
124+
}

0 commit comments

Comments
 (0)