Skip to content

Commit

Permalink
Add more tests and SwiftLint
Browse files Browse the repository at this point in the history
  • Loading branch information
quanvo87 committed Apr 12, 2017
1 parent 149bf1f commit cfa2fea
Show file tree
Hide file tree
Showing 49 changed files with 579 additions and 190 deletions.
9 changes: 9 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
included:
- Sources
- Tests
excluded:
- Packages
disabled_rules:
- trailing_whitespace
- line_length
- identifier_name
2 changes: 1 addition & 1 deletion Sources/Attachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ extension Attachment {
}

var isAlternative: Bool {
if case .html(let p) = type, p.alternative {
if case .html(let html) = type, html.alternative {
return true
}
return false
Expand Down
8 changes: 4 additions & 4 deletions Sources/AuthEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,23 @@ struct AuthEncoder {
let digest = CryptoUtils.hexString(from: hmac)
return ("\(user) \(digest)").base64Encoded
}

static func login(user: String, password: String) -> (encodedUser: String, encodedPassword: String) {
return (user.base64Encoded, password.base64Encoded)
}

static func plain(user: String, password: String) -> String {
let text = "\u{0000}\(user)\u{0000}\(password)"
return text.base64Encoded
}

static func xoauth2(user: String, accessToken: String) -> String {
let text = "user=\(user)\u{0001}auth=Bearer \(accessToken)\u{0001}\u{0001}"
return text.base64Encoded
}
}

private extension String {
extension String {
func base64Decoded() throws -> String {
guard let data = Data(base64Encoded: self), let base64Decoded = String(data: data, encoding: .utf8) else {
throw SMTPError(.base64DecodeFail(self))
Expand Down
13 changes: 9 additions & 4 deletions Sources/Command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,21 @@ enum Command {
case data
case dataEnd
case quit

var text: String {
switch self {
case .connect: return ""
case .ehlo(let domain): return "EHLO \(domain)"
case .helo(let domain): return "HELO \(domain)"
case .starttls: return "STARTTLS"

case .auth(let method, let credentials):
if let credentials = credentials { return "AUTH \(method.rawValue) \(credentials)" }
else { return "AUTH \(method.rawValue)" }
if let credentials = credentials {
return "AUTH \(method.rawValue) \(credentials)"
} else {
return "AUTH \(method.rawValue)"
}

case .authUser(let user): return user
case .authPassword(let password): return password
case .mail(let from): return "MAIL FROM: <\(from)>"
Expand All @@ -48,7 +53,7 @@ enum Command {
case .quit: return "QUIT"
}
}

var expectedResponseCodes: [ResponseCode] {
switch self {
case .connect: return [.serviceReady]
Expand Down
9 changes: 7 additions & 2 deletions Sources/Login.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ struct Login {
let group = DispatchGroup()
group.enter()

// Yet another thread is created here because trying to connect on a
// "bad" port will hang that thread. Doing this on a separate one
// ensures we can call `wait` and timeout if needed.
queue.async {
do {
try LoginHelper(hostname: self.hostname, user: self.user, password: self.password, port: self.port, ssl: self.ssl, authMethods: self.authMethods, domainName: self.domainName, accessToken: self.accessToken).login { (socket, err) in
Expand Down Expand Up @@ -133,8 +136,10 @@ private extension LoginHelper {
}

private func getServerInfo() throws -> [Response] {
do { return try ehlo() }
catch { return try helo() }
do { return try ehlo()
} catch {
return try helo()
}
}

private func doesStarttls(_ serverInfo: [Response]) throws -> Bool {
Expand Down
28 changes: 14 additions & 14 deletions Sources/Mail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ import Foundation

/// Represents an email that can be sent through an `SMTP` instance.
public struct Mail {

/// UUID of the `Mail`.
public let id = UUID().uuidString + ".Kitura-SMTP"

let from: User
let to: [User]
let cc: [User]?
Expand All @@ -31,7 +31,7 @@ public struct Mail {
let attachments: [Attachment]?
let alternative: Attachment?
let additionalHeaders: [String: String]?

/// Initializes a `Mail` object.
///
/// - Parameters:
Expand All @@ -50,7 +50,7 @@ public struct Mail {
self.bcc = bcc
self.subject = subject
self.text = text

if let attachments = attachments {
let result = attachments.takeLast { $0.isAlternative }
self.alternative = result.0
Expand All @@ -59,7 +59,7 @@ public struct Mail {
self.alternative = nil
self.attachments = nil
}

self.additionalHeaders = additionalHeaders
}
}
Expand All @@ -71,23 +71,23 @@ extension Mail {
fields["DATE"] = Date().smtpFormatted
fields["FROM"] = from.mime
fields["TO"] = to.map { $0.mime }.joined(separator: ", ")

if let cc = cc {
fields["CC"] = cc.map { $0.mime }.joined(separator: ", ")
}

fields["SUBJECT"] = subject.mimeEncoded ?? ""
fields["MIME-VERSION"] = "1.0 (Kitura-SMTP)"

if let additionalHeaders = additionalHeaders {
for (key, value) in additionalHeaders {
fields[key.uppercased()] = value
}
}

return fields
}

var headers: String {
return headersDictionary.map { (key, value) in
return "\(key): \(value)"
Expand All @@ -101,7 +101,7 @@ extension Mail {
}
}

private extension DateFormatter {
extension DateFormatter {
static let smtpDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en-US")
Expand All @@ -110,13 +110,13 @@ private extension DateFormatter {
}()
}

private extension Date {
extension Date {
var smtpFormatted: String {
return DateFormatter.smtpDateFormatter.string(from: self)
}
}

private extension Array {
extension Array {
func takeLast(where condition: (Element) -> Bool) -> (Element?, Array) {
var index: Int?
for i in (0 ..< count).reversed() {
Expand All @@ -125,7 +125,7 @@ private extension Array {
break
}
}

if let index = index {
var array = self
let ele = array.remove(at: index)
Expand Down
8 changes: 4 additions & 4 deletions Sources/Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct Response {
let code: ResponseCode
let message: String
let response: String

init(code: ResponseCode, message: String, response: String) {
self.code = code
self.message = message
Expand All @@ -31,16 +31,16 @@ struct Response {
struct ResponseCode: Equatable {
let rawValue: Int
init(_ value: Int) { rawValue = value }

static let serviceReady = ResponseCode(220)
static let connectionClosing = ResponseCode(221)
static let authSucceeded = ResponseCode(235)
static let commandOK = ResponseCode(250)
static let willForward = ResponseCode(251)
static let containingChallenge = ResponseCode(334)
static let startMailInput = ResponseCode(354)
public static func ==(lhs: ResponseCode, rhs: ResponseCode) -> Bool {

public static func==(lhs: ResponseCode, rhs: ResponseCode) -> Bool {
return lhs.rawValue == rhs.rawValue
}
}
48 changes: 28 additions & 20 deletions Sources/SMTPError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,40 @@ import LoggerAPI
/// Error type for KituraSMTP.
public enum SMTPError: Error, CustomStringConvertible {
// AuthCredentials
/// Creating the HMAC for CRAM-MD5 authentication failed.
/// Hashing server challenge with MD5 algorithm failed.
case md5HashChallengeFail
/// Decoding the base64 encoded String failed.
case base64DecodeFail(String)

// SMTP
/// An unknown errored occured while sending an email.
case unknownError
/// Error decoding string.
case base64DecodeFail(String)

// SMTPDataSender
/// File not found at path while trying to send a file `Attachment`.
// DataSender
/// File not found at path while trying to send file `Attachment`.
case fileNotFound(String)

// SMTPLogin
/// Connecting to server timed out. Check that you are connecting to a valid
/// `Port` for your SMTP server.
// Login
/// Could not connect to server within specified timeout. Ensure your
/// server can connect through `Port` 587 or specify which `Port` to connect
/// on. Some SMTP servers may require a longer timeout.
case couldNotConnectToServer(String, Int)

/// This SMTP server does not support any authentication methods that
/// were provided on initialization of this instance of `SMTP`.
/// The preferred `AuthMethod`s could not be found. Connecting with `SSL`
/// may be required.
case noSupportedAuthMethods(String)

/// Authenticating with XOAUTH2 requres a valid access token.
/// Attempted to login using `XOAUTH2` but `SMTP` instance was initialized
/// without an access token.
case noAccessToken

// Sender
/// Failed to create RegularExpression that can check if an email is valid.
case createEmailRegexFailed

// SMTP
/// An unknown errored occured while sending an email.
case unknownError

// SMTPSocket
/// Converting Data read from socket to a String failed.
/// Error converting Data read from socket to a String.
case convertDataUTF8Fail(Data)

/// Bad response received for command.
Expand All @@ -61,14 +68,15 @@ public enum SMTPError: Error, CustomStringConvertible {
switch self {
case .md5HashChallengeFail: return "Hashing server challenge with MD5 algorithm failed."
case .base64DecodeFail(let s): return "Error decoding string: \(s)."
case .unknownError: return "Unknown error occurred while trying to send mail."
case .fileNotFound(let p): return "File not found at path: \(p)."
case .fileNotFound(let p): return "File not found at path while trying to send file `Attachment`: \(p)."
case .couldNotConnectToServer(let s, let t): return "Could not connect to server (\(s)) within specified timeout (\(t) seconds). Ensure your server can connect through port 587 or specify which port to connect on. Some SMTP servers may require a longer timeout."
case .noSupportedAuthMethods(let hostname): return "The preferred authorization methods could not be found on \(hostname). Connecting with SSL may be required."
case .noAccessToken: return "Attempted to login using XOAUTH2 but SMTP instance was initialized without an access token."
case .convertDataUTF8Fail(let buf): return "Error converting data to string: \(buf)."
case .badResponse(let command, let response): return "Command (\(command)) returned bad response: \(response)."
case .invalidEmail(let email): return "Invalid email: \(email)."
case .createEmailRegexFailed: return "Failed to create RegularExpression that can check if an email is valid."
case .unknownError: return "An unknown errored occured while sending an email."
case .convertDataUTF8Fail(let buf): return "Error converting Data read from socket to a String: \(buf)."
case .badResponse(let command, let response): return "Bad response received for command. command: (\(command)), response: \(response)"
case .invalidEmail(let email): return "Invalid email provided for User: \(email)."
}
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/SMTPSocket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ extension SMTPSocket {
return validResponses
}

private static func getResponseCode(_ response: String, command: Command) throws -> ResponseCode {
static func getResponseCode(_ response: String, command: Command) throws -> ResponseCode {
guard response.characters.count >= 3 else {
throw SMTPError(.badResponse(command.text, response))
}
Expand All @@ -91,7 +91,7 @@ extension SMTPSocket {
return ResponseCode(responseCode)
}

private static func getResponseMessage(_ response: String) -> String {
static func getResponseMessage(_ response: String) -> String {
if response.characters.count < 4 { return "" }
let range = response.index(response.startIndex, offsetBy: 4)..<response.endIndex
return response[range]
Expand Down
19 changes: 10 additions & 9 deletions Sources/Sender.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,8 @@ private extension Sender {
}

private func validateEmails(_ emails: [String]) throws {
for email in emails {
if !email.isValidEmail {
throw SMTPError(.invalidEmail(email))
}
for email in emails where try !email.isValidEmail() {
throw SMTPError(.invalidEmail(email))
}
}

Expand Down Expand Up @@ -134,12 +132,15 @@ private extension Sender {
#endif

private extension Regex {
static let emailRegex = try! Regex(pattern: "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}", options: [])
static let emailRegex = try? Regex(pattern: "^[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}", options: [])
}

private extension String {
var isValidEmail: Bool {
let range = NSMakeRange(0, utf16.count)
return !Regex.emailRegex.matches(in: self, options: [], range: range).isEmpty
extension String {
func isValidEmail() throws -> Bool {
guard let emailRegex = Regex.emailRegex else {
throw SMTPError(.createEmailRegexFailed)
}
let range = NSRange(location: 0, length: utf16.count)
return !emailRegex.matches(in: self, options: [], range: range).isEmpty
}
}
2 changes: 1 addition & 1 deletion Sources/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public struct User {
/// - Parameters:
/// - name: Display name for the user. Defaults to nil.
/// - email: Email address for the user.
public init(name: String? = nil , email: String) {
public init(name: String? = nil, email: String) {
self.name = name
self.email = email
}
Expand Down
8 changes: 8 additions & 0 deletions Tests/KituraSMTPTests/Constant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ let text = "Humans and robots living together in harmony and equality. That was
let html = "<html><img src=\"http://vignette2.wikia.nocookie.net/megaman/images/4/40/StH250RobotMasters.jpg/revision/latest?cb=20130711161323\"/></html>"
let imgFilePath = root + "/x.png"

// https://www.base64decode.org/
let randomText1 = "Picture removal detract earnest is by. Esteems met joy attempt way clothes yet demesne tedious. Replying an marianne do it an entrance advanced. Two dare say play when hold. Required bringing me material stanhill jointure is as he. Mutual indeed yet her living result matter him bed whence."
let randomText1Encoded = "UGljdHVyZSByZW1vdmFsIGRldHJhY3QgZWFybmVzdCBpcyBieS4gRXN0ZWVtcyBtZXQgam95IGF0dGVtcHQgd2F5IGNsb3RoZXMgeWV0IGRlbWVzbmUgdGVkaW91cy4gUmVwbHlpbmcgYW4gbWFyaWFubmUgZG8gaXQgYW4gZW50cmFuY2UgYWR2YW5jZWQuIFR3byBkYXJlIHNheSBwbGF5IHdoZW4gaG9sZC4gUmVxdWlyZWQgYnJpbmdpbmcgbWUgbWF0ZXJpYWwgc3RhbmhpbGwgam9pbnR1cmUgaXMgYXMgaGUuIE11dHVhbCBpbmRlZWQgeWV0IGhlciBsaXZpbmcgcmVzdWx0IG1hdHRlciBoaW0gYmVkIHdoZW5jZS4="
let randomText2 = "Brillo viento gas esa contar hay. Alla no toda lune faro daba en pero. Ir rumiar altura id venian. El robusto hablado ya diarios tu hacerla mermado. Las sus renunciaba llamaradas misteriosa doscientas favorcillo dos pie. Una era fue pedirselos periodicos doscientas actualidad con. Exigian un en oh algunos adivino parezca notario yo. Eres oro dos mal lune vivo sepa les seda. Tio energia una esa abultar por tufillo sirenas persona suspiro. Me pandero tardaba pedirme puertas so senales la."
let randomText2Encoded = "QnJpbGxvIHZpZW50byBnYXMgZXNhIGNvbnRhciBoYXkuIEFsbGEgbm8gdG9kYSBsdW5lIGZhcm8gZGFiYSBlbiBwZXJvLiBJciBydW1pYXIgYWx0dXJhIGlkIHZlbmlhbi4gRWwgcm9idXN0byBoYWJsYWRvIHlhIGRpYXJpb3MgdHUgaGFjZXJsYSBtZXJtYWRvLiBMYXMgc3VzIHJlbnVuY2lhYmEgbGxhbWFyYWRhcyBtaXN0ZXJpb3NhIGRvc2NpZW50YXMgZmF2b3JjaWxsbyBkb3MgcGllLiBVbmEgZXJhIGZ1ZSBwZWRpcnNlbG9zIHBlcmlvZGljb3MgZG9zY2llbnRhcyBhY3R1YWxpZGFkIGNvbi4gRXhpZ2lhbiB1biBlbiBvaCBhbGd1bm9zIGFkaXZpbm8gcGFyZXpjYSBub3RhcmlvIHlvLiBFcmVzIG9ybyBkb3MgbWFsIGx1bmUgdml2byBzZXBhIGxlcyBzZWRhLiBUaW8gZW5lcmdpYSB1bmEgZXNhIGFidWx0YXIgcG9yIHR1ZmlsbG8gc2lyZW5hcyBwZXJzb25hIHN1c3Bpcm8uIE1lIHBhbmRlcm8gdGFyZGFiYSBwZWRpcm1lIHB1ZXJ0YXMgc28gc2VuYWxlcyBsYS4="
let randomText3 = "Intueor veritas suo majoris attinet rem res aggredi similia mei. Disputari abducerem ob ex ha interitum conflatos concipiam. Curam plura aequo rem etc serio fecto caput. Ea posterum lectorem remanere experiar videamus gi cognitum vi. Ad invenit accepit to petitis ea usitata ad. Hoc nam quibus hos oculis cumque videam ita. Res cau infinitum quadratam sanguinem."
let randomText3Encoded = "SW50dWVvciB2ZXJpdGFzIHN1byBtYWpvcmlzIGF0dGluZXQgcmVtIHJlcyBhZ2dyZWRpIHNpbWlsaWEgbWVpLiBEaXNwdXRhcmkgYWJkdWNlcmVtIG9iIGV4IGhhIGludGVyaXR1bSBjb25mbGF0b3MgY29uY2lwaWFtLiBDdXJhbSBwbHVyYSBhZXF1byByZW0gZXRjIHNlcmlvIGZlY3RvIGNhcHV0LiBFYSBwb3N0ZXJ1bSBsZWN0b3JlbSByZW1hbmVyZSBleHBlcmlhciB2aWRlYW11cyBnaSBjb2duaXR1bSB2aS4gQWQgaW52ZW5pdCBhY2NlcGl0IHRvIHBldGl0aXMgZWEgdXNpdGF0YSBhZC4gSG9jIG5hbSBxdWlidXMgaG9zIG9jdWxpcyBjdW1xdWUgdmlkZWFtIGl0YS4gUmVzIGNhdSBpbmZpbml0dW0gcXVhZHJhdGFtIHNhbmd1aW5lbS4="

private extension Int {
static func randomEmailNum(_ max: Int) -> String {
#if os(Linux)
Expand Down
Loading

0 comments on commit cfa2fea

Please sign in to comment.