Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
7 changed files
with
400 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
// | ||
// MailgunClient.swift | ||
// Mailgun | ||
// | ||
// Created by Anthony Castelli on 11/14/16. | ||
// | ||
// | ||
|
||
import Console | ||
import Vapor | ||
import HTTP | ||
import Foundation | ||
import SMTP | ||
import Mail | ||
|
||
|
||
/** | ||
Mailgun client | ||
*/ | ||
public final class MailgunClient { | ||
|
||
static var defaultApiKey: String? | ||
static var defaultDomain: String? | ||
static var defaultClient: ClientProtocol.Type? | ||
|
||
var domain: String | ||
var apiKey: String | ||
var client: ClientProtocol | ||
|
||
public init(clientProtocol: ClientProtocol.Type, domain: String, apiKey: String) throws { | ||
self.domain = domain | ||
self.apiKey = apiKey | ||
self.client = try clientProtocol.make(scheme: "https", host: "api.mailgun.net") | ||
} | ||
|
||
public func send(_ emails: [MailgunEmail]) throws { | ||
try emails.forEach { email in | ||
let boundary = "vapor.mailgun.package.\(SecureToken().token)" | ||
let bytes = try createMultipartData(email.makeNode(), boundary: boundary) | ||
let headers: [HeaderKey : String] = [ | ||
"Authorization": authorizationHeaderValue(apiKey), | ||
"Content-Type": "multipart/form-data; boundary=\(boundary)" | ||
] | ||
let response = try client.post(path: "/v3/\(domain)/messages", headers: headers, body: Body.data(bytes)) | ||
switch response.status.statusCode { | ||
case 200, 202: return | ||
case 400: throw MailgunError.badRequest(try response.json?.extract()) | ||
case 401: throw MailgunError.unauthorized | ||
case 500, 503: throw MailgunError.serverError | ||
default: throw MailgunError.unexpectedServerResponse | ||
} | ||
} | ||
} | ||
|
||
fileprivate func authorizationHeaderValue(_ apiKey: String) -> String { | ||
let userPasswordString = "api:\(apiKey)" | ||
guard let userPasswordData = userPasswordString.data(using: String.Encoding.utf8) else { return "" } | ||
let base64EncodedCredential = userPasswordData.base64EncodedString() | ||
let authString = "Basic \(base64EncodedCredential)" | ||
return authString | ||
} | ||
|
||
fileprivate func createMultipartData(_ node: Node, boundary: String) throws -> Bytes { | ||
var serialized = "" | ||
|
||
guard let object = node.object else { throw MailgunError.missingEmailContent } | ||
object.forEach { key, value in | ||
guard let value = value.string else { return } | ||
serialized += "--\(boundary)\r\n" | ||
serialized += "Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n" | ||
serialized += "\(value)\r\n" | ||
} | ||
|
||
serialized += "--\(boundary)--\r\n" | ||
return serialized.bytes | ||
} | ||
|
||
} | ||
|
||
extension MailgunClient: MailClientProtocol { | ||
|
||
public static func configure(_ config: Settings.Config) throws { | ||
guard let mg = config["mailgun"]?.object else { | ||
throw MailgunError.noMailgunConfig | ||
} | ||
guard let domain = mg["domain"]?.string else { | ||
throw MailgunError.missingConfig("domain") | ||
} | ||
guard let apiKey = mg["apiKey"]?.string else { | ||
throw MailgunError.missingConfig("apiKey") | ||
} | ||
defaultDomain = domain | ||
defaultApiKey = apiKey | ||
} | ||
|
||
public static func boot(_ drop: Vapor.Droplet) { | ||
self.defaultClient = drop.client | ||
} | ||
|
||
public convenience init() throws { | ||
guard let client = MailgunClient.defaultClient else { | ||
throw MailgunError.noClient | ||
} | ||
guard let domain = MailgunClient.defaultDomain else { | ||
throw MailgunError.missingConfig("domain") | ||
} | ||
guard let apiKey = MailgunClient.defaultApiKey else { | ||
throw MailgunError.missingConfig("apiKey") | ||
} | ||
try self.init(clientProtocol: client, domain: domain, apiKey: apiKey) | ||
} | ||
|
||
public func send(_ emails: [SMTP.Email]) throws { | ||
// Convert to Mailgun Emails and then send | ||
let mgEmails = emails.map { MailgunEmail(from: $0 ) } | ||
try send(mgEmails) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
// | ||
// MailgunError.swift | ||
// | ||
// Created by Anthony Castelli on 3/27/17. | ||
// | ||
// | ||
|
||
import Foundation | ||
import Node | ||
|
||
/* | ||
An error, either in configuration or in execution. | ||
*/ | ||
public enum MailgunError: Swift.Error { | ||
|
||
public struct ErrorInfo: NodeInitializable { | ||
public let message: String | ||
public let id: String? | ||
|
||
public init(node: Node, in context: Context) throws { | ||
message = try node.extract("message") | ||
id = try node.extract("id") | ||
} | ||
} | ||
|
||
/* | ||
No configuration for Mailgun could be found at all. | ||
*/ | ||
case noMailgunConfig | ||
|
||
/* | ||
A required configuration key was missing. The associated value is the | ||
name of the missing key. | ||
*/ | ||
case missingConfig(String) | ||
|
||
/* | ||
MailgunClient was instantiated without a Vapor Client. This would | ||
normally be set via Provider, but if you are instantiating directly, | ||
you must pass or set the client protocol first. | ||
*/ | ||
case noClient | ||
|
||
/* | ||
There was a problem with your request. | ||
*/ | ||
case badRequest(ErrorInfo?) | ||
|
||
/* | ||
You do not have authorization to make the request. | ||
*/ | ||
case unauthorized | ||
|
||
/* | ||
An error occurred on a Mailgun server, or the Mailgun v3 API is | ||
not available. | ||
*/ | ||
case serverError | ||
|
||
/* | ||
Missing email content | ||
*/ | ||
case missingEmailContent | ||
|
||
case unexpectedServerResponse | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
// | ||
// MailgunEmail.swift | ||
// Mailgun | ||
// | ||
// Created by Anthony Castelli on 11/14/16. | ||
// | ||
// | ||
|
||
import Foundation | ||
import SMTP | ||
import Node | ||
|
||
public final class MailgunEmail { | ||
|
||
public var from: EmailAddressRepresentable | ||
public var to: [EmailAddress] | ||
public var subject: String? | ||
public var body: EmailBody | ||
|
||
public init(to: [EmailAddress], from: EmailAddressRepresentable, subject: String?, body: EmailBody) { | ||
self.to = to | ||
self.from = from | ||
self.subject = subject | ||
self.body = body | ||
} | ||
|
||
internal func dictionaryRepresentation() -> [String : String] { | ||
var data = [String : String]() | ||
data["from"] = self.from.emailAddress.address | ||
data["to"] = (self.to.map { $0.emailAddress.address }).joined(separator: ",") | ||
data["subject"] = self.subject | ||
switch self.body.type { | ||
case .plain: data["text"] = self.body.content | ||
case .html: data["html"] = self.body.content | ||
} | ||
return data | ||
} | ||
|
||
} | ||
|
||
extension MailgunEmail { | ||
|
||
/* | ||
Convert a basic Vapor SMTP.Email into a MailgunEmail | ||
*/ | ||
public convenience init(from: Email) { | ||
self.init(to: from.to, from: from.from, subject: from.subject, body: from.body) | ||
} | ||
|
||
} | ||
|
||
extension MailgunEmail: NodeRepresentable { | ||
|
||
public func makeNode(context: Context) throws -> Node { | ||
var obj = Node([:]) | ||
// From | ||
obj["from"] = self.from.emailAddress.address.makeNode() | ||
|
||
// To | ||
obj["to"] = (self.to.map { $0.emailAddress.address }).joined(separator: ",").makeNode() | ||
|
||
// Subject | ||
if let subject = subject { | ||
obj["subject"] = Node(subject) | ||
} | ||
|
||
// Body | ||
switch self.body.type { | ||
case .plain: obj["text"] = self.body.content.makeNode() | ||
case .html: obj["html"] = self.body.content.makeNode() | ||
} | ||
return obj | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
// | ||
// SecureToken.swift | ||
// | ||
// Created by Anthony Castelli on 3/28/17. | ||
// | ||
// | ||
|
||
import Foundation | ||
|
||
/** Coppied from TurstyleCrypto. | ||
* Reason for this is to elimite the requirement on Tunrstyle per Vapor 2.0 | ||
**/ | ||
public class SecureToken { | ||
private let file = fopen("/dev/urandom", "r") | ||
|
||
/// Initialize URandom | ||
public init() {} | ||
|
||
deinit { | ||
fclose(file) | ||
} | ||
|
||
private func read(numBytes: Int) -> [Int8] { | ||
/// Initialize an empty array with numBytes+1 for null terminated string | ||
var bytes = [Int8](repeating: 0, count: numBytes) | ||
fread(&bytes, 1, numBytes, file) | ||
|
||
return bytes | ||
} | ||
|
||
/// Get a byte array of random UInt8s | ||
public func random(numBytes: Int) -> [UInt8] { | ||
return unsafeBitCast(read(numBytes: numBytes), to: [UInt8].self) | ||
} | ||
|
||
/// Get a random string usable for authentication purposes | ||
public var token: String { | ||
return Data(bytes: random(numBytes: 16)).base64UrlEncodedString | ||
} | ||
|
||
} | ||
|
||
extension Data { | ||
var base64UrlEncodedString: String { | ||
return base64EncodedString() | ||
.replacingOccurrences(of: "=", with: "") | ||
.replacingOccurrences(of: "+", with: "-") | ||
.replacingOccurrences(of: "/", with: "_") | ||
} | ||
} |
Oops, something went wrong.