From befbbab17246197fe7415bc5d90b1f085122cc8e Mon Sep 17 00:00:00 2001 From: Mihael Isaev Date: Tue, 14 Jan 2020 01:47:54 +0400 Subject: [PATCH 01/15] Upgrade to Vapor4 --- .gitignore | 1 + Package.swift | 7 +- README.md | 185 +++++++++------- Sources/Mailgun/Enums/Error.swift | 50 +++++ Sources/Mailgun/Enums/Region.swift | 5 + Sources/Mailgun/Mailgun.swift | 221 +++++++------------ Sources/Mailgun/Models/Configuration.swift | 49 ++++ Sources/Mailgun/Models/ErrorResponse.swift | 5 + Sources/Mailgun/Models/IncomingMessage.swift | 36 +-- Sources/Mailgun/Models/Message.swift | 134 ++++++----- Sources/Mailgun/Models/RouteSetup.swift | 4 +- Sources/Mailgun/Models/Template.swift | 54 +++-- Sources/Mailgun/Models/TemplateMessage.swift | 196 ++++++++-------- Tests/MailgunTests/MailgunTests.swift | 2 +- 14 files changed, 519 insertions(+), 430 deletions(-) create mode 100644 Sources/Mailgun/Enums/Error.swift create mode 100644 Sources/Mailgun/Enums/Region.swift create mode 100644 Sources/Mailgun/Models/Configuration.swift create mode 100644 Sources/Mailgun/Models/ErrorResponse.swift diff --git a/.gitignore b/.gitignore index 7fb530a..5d6d3d7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /Packages /*.xcodeproj Package.resolved +.swiftpm \ No newline at end of file diff --git a/Package.swift b/Package.swift index fc3ebe0..ab853cf 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,13 @@ -// swift-tools-version:4.0 +// swift-tools-version:5.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "Mailgun", + platforms: [ + .macOS(.v10_15) + ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( @@ -12,7 +15,7 @@ let package = Package( targets: ["Mailgun"]), ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "3.3.0") + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-beta.3") ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. diff --git a/README.md b/README.md index 5ad6c8d..d474a00 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,23 @@ [![Slack](https://img.shields.io/badge/join-slack-745EAF.svg?style=flat)](https://vapor.team) [![Platforms](https://img.shields.io/badge/platforms-macOS%2010.13%20|%20Ubuntu%2016.04%20LTS-ff0000.svg?style=flat)](http://cocoapods.org/pods/FASwift) -[![Swift 4.1](https://img.shields.io/badge/swift-4.1-orange.svg?style=flat)](http://swift.org) -[![Vapor 3](https://img.shields.io/badge/vapor-3.0-blue.svg?style=flat)](https://vapor.codes) +[![Swift 5.1](https://img.shields.io/badge/swift-4.1-orange.svg?style=flat)](http://swift.org) +[![Vapor 4](https://img.shields.io/badge/vapor-3.0-blue.svg?style=flat)](https://vapor.codes) ## -`Mailgun` is a Vapor 3 service for a popular [email sending API](https://www.mailgun.com/) +`Mailgun` is a Vapor 4 service for a popular [email sending API](https://www.mailgun.com/) +> Note: Vapor3 version is available in `vapor3` branch and from `1.5.0` tag ## Installation Vapor Mailgun Service can be installed with Swift Package Manager ```swift -.package(url: "https://github.com/twof/VaporMailgunService.git", from: "1.5.0") +.package(url: "https://github.com/twof/VaporMailgunService.git", from: "2.0.0") + +//and in targets add +//"Mailgun" ``` ## Usage @@ -27,10 +31,24 @@ Make sure you get an API key and register a custom domain In `configure.swift`: ```swift -let mailgun = Mailgun(apiKey: "", domain: "mg.example.com", region: .eu) -services.register(mailgun, as: Mailgun.self) +import FCM + +// Called before your application initializes. +func configure(_ app: Application) throws { + /// case 1 + /// put into your environment variables the following keys: + /// MAILGUN_API_KEY=... + /// MAILGUN_DOMAIN=... + /// MAILGUN_REGION=... + app.fcm.configuration = .environment + + /// case 2 + /// manually + app.mailgun.configuration = .init(apiKey: "", domain: "mg.example.com", region: .eu) +} ``` + > Note: If your private api key begins with `key-`, be sure to include it ### Use @@ -40,57 +58,66 @@ In `routes.swift`: #### Without attachments ```swift -router.post("mail") { (req) -> Future in - let message = Mailgun.Message( - from: "postmaster@example.com", - to: "example@gmail.com", - subject: "Newsletter", - text: "This is a newsletter", - html: "

This is a newsletter

" - ) - - let mailgun = try req.make(Mailgun.self) - return try mailgun.send(message, on: req) +import Mailgun + +func routes(_ app: Application) throws { + app.post("mail") { req -> EventLoopFuture in + let message = MailgunMessage( + from: "postmaster@example.com", + to: "example@gmail.com", + subject: "Newsletter", + text: "This is a newsletter", + html: "

This is a newsletter

" + ) + return req.mailgun.send(message) + } } ``` #### With attachments ```swift -router.post("mail") { (req) -> Future in - let fm = FileManager.default - guard let attachmentData = fm.contents(atPath: "/tmp/test.pdf") else { - throw Abort(.internalServerError) +import Mailgun + +func routes(_ app: Application) throws { + app.post("mail") { req -> EventLoopFuture in + let fm = FileManager.default + guard let attachmentData = fm.contents(atPath: "/tmp/test.pdf") else { + throw Abort(.internalServerError) + } + let bytes: [UInt8] = Array(attachmentData) + var bytesBuffer = ByteBufferAllocator().buffer(capacity: bytes.count) + bytesBuffer.writeBytes(bytes) + let attachment = File.init(data: bytesBuffer, filename: "test.pdf") + let message = MailgunMessage( + from: "postmaster@example.com", + to: "example@gmail.com", + subject: "Newsletter", + text: "This is a newsletter", + html: "

This is a newsletter

", + attachments: [attachment] + ) + return req.mailgun.send(message) } - let attachment = File(data: attachmentData, filename: "test.pdf") - let message = Mailgun.Message( - from: "postmaster@example.com", - to: "example@gmail.com", - subject: "Newsletter", - text: "This is a newsletter", - html: "

This is a newsletter

", - attachments: [attachment] - ) - - let mailgun = try req.make(Mailgun.self) - return try mailgun.send(message, on: req) } ``` #### With template (attachments can be used in same way) ```swift -router.post("mail") { (req) -> Future in - let message = Mailgun.TemplateMessage( - from: "postmaster@example.com", - to: "example@gmail.com", - subject: "Newsletter", - template: "my-template", - templateData: ["foo": "bar"] - ) - - let mailgun = try req.make(Mailgun.self) - return try mailgun.send(message, on: req) +import Mailgun + +func routes(_ app: Application) throws { + app.post("mail") { req -> EventLoopFuture in + let message = MailgunTemplateMessage( + from: "postmaster@example.com", + to: "example@gmail.com", + subject: "Newsletter", + template: "my-template", + templateData: ["foo": "bar"] + ) + return req.mailgun.send(message) + } } ``` @@ -111,33 +138,35 @@ First setup a leaf file in `Resources/Views/Emails/my-email.leaf` With this, you can change the `#(name)` with a variable from your Swift code, when sending the mail ```swift -router.post("mail") { (req) -> Future in - let content = try req.view().render("Emails/my-email", [ - "name": "Bob" - ]) - - let message = Mailgun.Message( - from: "postmaster@example.com", - to: "example@gmail.com", - subject: "Newsletter", - text: "", - html: content - ) - - let mailgun = try req.make(Mailgun.self) - return try mailgun.send(message, on: req) +import Mailgun + +func routes(_ app: Application) throws { + app.post("mail") { req -> EventLoopFuture in + let content = try req.view().render("Emails/my-email", [ + "name": "Bob" + ]) + + let message = Mailgun.Message( + from: "postmaster@example.com", + to: "example@gmail.com", + subject: "Newsletter", + text: "", + html: content + ) + + return req.mailgun.send(message) + } } ``` #### Setup routes ```swift -public func boot(_ app: Application) throws { +public func configure(_ app: Application) throws { // sets up a catch_all forward for the route listed - let routeSetup = RouteSetup(forwardURL: "http://example.com/mailgun/all", description: "A route for all emails") - let mailgunClient = try app.make(Mailgun.self) - try mailgunClient.setup(forwarding: routeSetup, with: app).map { (resp) in - print(resp) + let routeSetup = MailgunRouteSetup(forwardURL: "http://example.com/mailgun/all", description: "A route for all emails") + try app.mailgun.setup(forwarding: routeSetup).map { response in + print(response) } } ``` @@ -145,13 +174,18 @@ public func boot(_ app: Application) throws { #### Handle routes ```swift -mailgunGroup.post("all") { (req) -> Future in - do { - return try req.content.decode(IncomingMailgun.self).map { (incomingMail) in +import Mailgun + +func routes(_ app: Application) throws { + let mailgunGroup = app.grouped("mailgun") + mailgunGroup.post("all") { req -> String in + do { + let incomingMail = try req.content.decode(MailgunIncomingMessage.self) + print("incomingMail: (incomingMail)") return "Hello" + } catch { + throw Abort(.internalServerError, reason: "Could not decode incoming message") } - } catch { - throw Abort(HTTPStatus.internalServerError, reason: "Could not decode incoming message") } } ``` @@ -159,10 +193,13 @@ mailgunGroup.post("all") { (req) -> Future in #### Creating templates ```swift -router.post("template") { (req) -> Future in - let template = Mailgun.Template(name: "my-template", description: "api created :)", template: "

Hello {{ name }}

") - - let mailgun = try req.make(Mailgun.self) - return try mailgun.createTemplate(template, on: req) +import Mailgun + +func routes(_ app: Application) throws { + let mailgunGroup = app.grouped("mailgun") + mailgunGroup.post("template") { req -> EventLoopFuture in + let template = MailgunTemplate(name: "my-template", description: "api created :)", template: "

Hello {{ name }}

") + return req.mailgun.createTemplate(template) + } } ``` diff --git a/Sources/Mailgun/Enums/Error.swift b/Sources/Mailgun/Enums/Error.swift new file mode 100644 index 0000000..6d2bd5c --- /dev/null +++ b/Sources/Mailgun/Enums/Error.swift @@ -0,0 +1,50 @@ +import Vapor + +public enum MailgunError: Error { + /// Encoding problem + case encodingProblem + + /// Failed authentication + case authenticationFailed + + /// Failed to send email (with error message) + case unableToSendEmail(MailgunErrorResponse) + + /// Failed to create template (with error message) + case unableToCreateTemplate(MailgunErrorResponse) + + /// Generic error + case unknownError(ClientResponse) + + /// Identifier + public var identifier: String { + switch self { + case .encodingProblem: + return "mailgun.encoding_error" + case .authenticationFailed: + return "mailgun.auth_failed" + case .unableToSendEmail: + return "mailgun.send_email_failed" + case .unableToCreateTemplate: + return "mailgun.create_template_failed" + case .unknownError: + return "mailgun.unknown_error" + } + } + + /// Reason + public var reason: String { + switch self { + case .encodingProblem: + return "Encoding problem" + case .authenticationFailed: + return "Failed authentication" + case .unableToSendEmail(let err): + return "Failed to send email (\(err.message))" + case .unableToCreateTemplate(let err): + return "Failed to create template (\(err.message))" + case .unknownError: + return "Generic error" + } + } +} diff --git a/Sources/Mailgun/Enums/Region.swift b/Sources/Mailgun/Enums/Region.swift new file mode 100644 index 0000000..f7ad926 --- /dev/null +++ b/Sources/Mailgun/Enums/Region.swift @@ -0,0 +1,5 @@ +/// Describes a region: US or EU +public enum MailgunRegion: String { + case us + case eu +} diff --git a/Sources/Mailgun/Mailgun.swift b/Sources/Mailgun/Mailgun.swift index dbb80f6..0897305 100644 --- a/Sources/Mailgun/Mailgun.swift +++ b/Sources/Mailgun/Mailgun.swift @@ -4,129 +4,62 @@ import Foundation // MARK: - Service -public protocol MailgunProvider: Service { - var apiKey: String { get } - var domain: String { get } - var region: Mailgun.Region { get } - func send(_ content: Mailgun.Message, on container: Container) throws -> Future - func send(_ content: Mailgun.TemplateMessage, on container: Container) throws -> Future - func setup(forwarding: RouteSetup, with container: Container) throws -> Future - func createTemplate(_ template: Mailgun.Template, on container: Container) throws -> Future +public protocol MailgunProvider { + var configuration: MailgunConfiguration? { get set } + func send(_ content: MailgunMessage) throws -> EventLoopFuture + func send(_ content: MailgunTemplateMessage) throws -> EventLoopFuture + func setup(forwarding: MailgunRouteSetup) throws -> EventLoopFuture + func createTemplate(_ template: MailgunTemplate) throws -> EventLoopFuture } -// MARK: - Engine - public struct Mailgun: MailgunProvider { + let application: Application - /// Describes a region: US or EU - public enum Region { - case us - case eu - } + // MARK: Initialization - public enum Error: Debuggable { - - /// Encoding problem - case encodingProblem - - /// Failed authentication - case authenticationFailed - - /// Failed to send email (with error message) - case unableToSendEmail(ErrorResponse) + public init (_ app: Application) { + application = app + } +} - /// Failed to create template (with error message) - case unableToCreateTemplate(ErrorResponse) +// MARK: - Configuration - /// Generic error - case unknownError(Response) - - /// Identifier - public var identifier: String { - switch self { - case .encodingProblem: - return "mailgun.encoding_error" - case .authenticationFailed: - return "mailgun.auth_failed" - case .unableToSendEmail: - return "mailgun.send_email_failed" - case .unableToCreateTemplate: - return "mailgun.create_template_failed" - case .unknownError: - return "mailgun.unknown_error" - } - } - - /// Reason - public var reason: String { - switch self { - case .encodingProblem: - return "Encoding problem" - case .authenticationFailed: - return "Failed authentication" - case .unableToSendEmail(let err): - return "Failed to send email (\(err.message))" - case .unableToCreateTemplate(let err): - return "Failed to create template (\(err.message))" - case .unknownError: - return "Generic error" - } - } +extension Mailgun { + struct ConfigurationKey: StorageKey { + typealias Value = MailgunConfiguration } - - /// Error response object - public struct ErrorResponse: Decodable { - - /// Error messsage - public let message: String - - } - - /// API key (including "key-" prefix) - public let apiKey: String - - /// Domain - public let domain: String - - /// Region - public let region: Mailgun.Region - - // MARK: Initialization - - - /// Initializer - /// - /// - Parameters: - /// - apiKey: API key including "key-" prefix - /// - domain: API domain - public init(apiKey: String, domain: String, region: Mailgun.Region) { - self.apiKey = apiKey - self.domain = domain - self.region = region + + public var configuration: MailgunConfiguration? { + get { + application.storage[ConfigurationKey.self] + } + nonmutating set { + application.storage[ConfigurationKey.self] = newValue + } } - - // MARK: Send message - +} + +// MARK: - Send message + +extension Mailgun { /// Send message /// /// - Parameters: /// - content: Message /// - container: Container /// - Returns: Future - public func send(_ content: Message, on container: Container) throws -> Future { - return try postRequest(content, endpoint: "messages", on: container) + public func send(_ content: MailgunMessage) -> EventLoopFuture { + postRequest(content, endpoint: "messages") } - // MARK: Send message - /// Send message /// /// - Parameters: /// - content: TemplateMessage /// - container: Container /// - Returns: Future - public func send(_ content: TemplateMessage, on container: Container) throws -> Future { - return try postRequest(content, endpoint: "messages", on: container) + public func send(_ content: MailgunTemplateMessage) -> EventLoopFuture { + postRequest(content, endpoint: "messages") } /// Setup forwarding @@ -135,8 +68,8 @@ public struct Mailgun: MailgunProvider { /// - setup: RouteSetup /// - container: Container /// - Returns: Future - public func setup(forwarding setup: RouteSetup, with container: Container) throws -> Future { - return try postRequest(setup, endpoint: "v3/routes", on: container) + public func setup(forwarding setup: MailgunRouteSetup) -> EventLoopFuture { + postRequest(setup, endpoint: "v3/routes") } /// Create template @@ -145,73 +78,77 @@ public struct Mailgun: MailgunProvider { /// - template: Template /// - container: Container /// - Returns: Future - public func createTemplate(_ template: Template, on container: Container) throws -> Future { - return try postRequest(template, endpoint: "templates", on: container) + public func createTemplate(_ template: MailgunTemplate) -> EventLoopFuture { + postRequest(template, endpoint: "templates") } } -// MARK: Private +extension Application { + public var mailgun: Mailgun { .init(self) } +} + +extension Request { + public var mailgun: Mailgun { .init(application) } +} + +// MARK: - Private fileprivate extension Mailgun { - private var baseApiUrl: String { - return region == .eu ? "https://api.eu.mailgun.net/v3" : "https://api.mailgun.net/v3" - } - func encode(apiKey: String) throws -> String { guard let apiKeyData = "api:\(apiKey)".data(using: .utf8) else { - throw Error.encodingProblem + throw MailgunError.encodingProblem } let authKey = apiKeyData.base64EncodedData() guard let authKeyEncoded = String.init(data: authKey, encoding: .utf8) else { - throw Error.encodingProblem + throw MailgunError.encodingProblem } - return authKeyEncoded } + + private func postRequest(_ content: Message, endpoint: String) -> EventLoopFuture { + guard let configuration = self.configuration else { + fatalError("Mailgun not configured. Use app.mailgun.configuration = ...") + } + + return application.eventLoopGroup.future().flatMapThrowing { _ -> HTTPHeaders in + let authKeyEncoded = try self.encode(apiKey: configuration.apiKey) + var headers = HTTPHeaders() + headers.add(name: .authorization, value: "Basic \(authKeyEncoded)") + return headers + }.flatMap { headers in + let mailgunURI = URI(string: "\(configuration.baseApiUrl)/\(configuration.domain)/\(endpoint)") + return self.application.client.post(mailgunURI, headers: headers) { req in + try req.content.encode(content) + }.flatMapThrowing { + try self.process($0) + } + } + } - private func process(_ response: Response) throws -> Response { + private func process(_ response: ClientResponse) throws -> ClientResponse { switch true { - case response.http.status.code == HTTPStatus.ok.code: + case response.status == .ok: return response - case response.http.status.code == HTTPStatus.unauthorized.code: - throw Error.authenticationFailed + case response.status == .unauthorized: + throw MailgunError.authenticationFailed default: - if let data = response.http.body.data, let err = (try? JSONDecoder().decode(ErrorResponse.self, from: data)) { - if (err.message.hasPrefix("template")) { - throw Error.unableToCreateTemplate(err) + if let body = response.body, let err = try? JSONDecoder().decode(MailgunErrorResponse.self, from: body) { + if err.message.hasPrefix("template") { + throw MailgunError.unableToCreateTemplate(err) } else { - throw Error.unableToSendEmail(err) + throw MailgunError.unableToSendEmail(err) } } - throw Error.unknownError(response) - } - } - - private func postRequest(_ content: Message, endpoint: String, on container: Container) throws -> Future { - let authKeyEncoded = try encode(apiKey: self.apiKey) - - var headers = HTTPHeaders([]) - headers.add(name: HTTPHeaderName.authorization, value: "Basic \(authKeyEncoded)") - - let mailgunURL = "\(baseApiUrl)/\(domain)/\(endpoint)" - - let client = try container.make(Client.self) - - return client.post(mailgunURL, headers: headers) { req in - try req.content.encode(content) - }.map { response in - try self.process(response) + throw MailgunError.unknownError(response) } } - } // MARK: - Conversions -extension Array where Element == Mailgun.Message.FullEmail { - +extension Array where Element == MailgunMessage.FullEmail { var stringArray: [String] { - return map { entry -> String in + map { entry in guard let name = entry.name else { return entry.email } diff --git a/Sources/Mailgun/Models/Configuration.swift b/Sources/Mailgun/Models/Configuration.swift new file mode 100644 index 0000000..c1ee942 --- /dev/null +++ b/Sources/Mailgun/Models/Configuration.swift @@ -0,0 +1,49 @@ +import Foundation +import Vapor + +public struct MailgunConfiguration { + /// API key (including "key-" prefix) + public let apiKey: String + + /// Domain + public let domain: String + + /// Region + public let region: MailgunRegion + + /// Initializer + /// + /// - Parameters: + /// - apiKey: API key including "key-" prefix + /// - domain: API domain + public init(apiKey: String, domain: String, region: MailgunRegion) { + self.apiKey = apiKey + self.domain = domain + self.region = region + } + + var baseApiUrl: String { + switch region { + case .us: return "https://api.mailgun.net/v3" + case .eu: return "https://api.eu.mailgun.net/v3" + } + } + + /// It will try to initialize configuration with environment variables: + /// - MG_KEY + /// - MG_DOMAIN + /// - MG_REGION + public static var environment: MailgunConfiguration { + guard + let apiKey = Environment.get("MAILGUN_API_KEY"), + let domain = Environment.get("MAILGUN_DOMAIN"), + let rawRegion = Environment.get("MAILGUN_REGION") + else { + fatalError("Mailgun environmant variables not set") + } + guard let region = MailgunRegion(rawValue: rawRegion.lowercased()) else { + fatalError("Mailgun unable to parse environmant region value") + } + return .init(apiKey: apiKey, domain: domain, region: region) + } +} diff --git a/Sources/Mailgun/Models/ErrorResponse.swift b/Sources/Mailgun/Models/ErrorResponse.swift new file mode 100644 index 0000000..cb8e261 --- /dev/null +++ b/Sources/Mailgun/Models/ErrorResponse.swift @@ -0,0 +1,5 @@ +/// Error response object +public struct MailgunErrorResponse: Decodable { + /// Error messsage + public let message: String +} diff --git a/Sources/Mailgun/Models/IncomingMessage.swift b/Sources/Mailgun/Models/IncomingMessage.swift index d90fad3..c6101ca 100644 --- a/Sources/Mailgun/Models/IncomingMessage.swift +++ b/Sources/Mailgun/Models/IncomingMessage.swift @@ -1,9 +1,9 @@ import Vapor -public struct IncomingMailgun: Content { - public static var defaultContentType: MediaType = .formData +public struct MailgunIncomingMessage: Content { + public static var defaultContentType: HTTPMediaType = .formData - public let recipient: String + public let recipients: String public let sender: String public let from: String public let subject: String @@ -12,16 +12,12 @@ public struct IncomingMailgun: Content { public let strippedSignature: String? public let bodyHTML: String public let strippedHTML: String - public let attachmentCount: Int - public let timestamp: Int - public let token: String - public let signature: String public let messageHeaders: String public let contentIdMap: String - public let attachment: String? + public let attachments: [Attachment]? enum CodingKeys: String, CodingKey { - case recipient + case recipients case sender case from case subject @@ -30,12 +26,24 @@ public struct IncomingMailgun: Content { case strippedSignature = "stripped-signiture" case bodyHTML = "body-html" case strippedHTML = "stripped-html" - case attachmentCount = "attachment-count" - case timestamp - case token - case signature case messageHeaders = "message-headers" case contentIdMap = "content-id-map" - case attachment = "attachment-x" + case attachments + } +} + +extension MailgunIncomingMessage { + public struct Attachment: Codable { + public let size: Int64 + public let url: String + public let name: String + public let contentType: String + + enum CodingKeys: String, CodingKey { + case size + case url + case name + case contentType = "content-type" + } } } diff --git a/Sources/Mailgun/Models/Message.swift b/Sources/Mailgun/Models/Message.swift index a007ad9..0e4085e 100644 --- a/Sources/Mailgun/Models/Message.swift +++ b/Sources/Mailgun/Models/Message.swift @@ -1,73 +1,71 @@ import Vapor -extension Mailgun { - public struct Message: Content { - public static var defaultContentType: MediaType = .formData - - public typealias FullEmail = (email: String, name: String?) - - public let from: String - public let to: String - public let replyTo: String? - public let cc: String? - public let bcc: String? - public let subject: String - public let text: String - public let html: String? - public let attachment: [File]? - public let inline: [File]? - - private enum CodingKeys: String, CodingKey { - case from - case to - case replyTo = "h:Reply-To" - case cc - case bcc - case subject - case text - case html - case attachment - case inline - } - - public init(from: String, to: String, replyTo: String? = nil, cc: String? = nil, bcc: String? = nil, subject: String, text: String, html: String? = nil, attachments: [File]? = nil, inline: [File]? = nil) { - self.from = from - self.to = to - self.replyTo = replyTo - self.cc = cc - self.bcc = bcc - self.subject = subject - self.text = text - self.html = html - self.attachment = attachments - self.inline = inline - } - - public init(from: String, to: [String], replyTo: String? = nil, cc: [String]? = nil, bcc: [String]? = nil, subject: String, text: String, html: String? = nil, attachments: [File]? = nil, inline: [File]? = nil) { - self.from = from - self.to = to.joined(separator: ",") - self.replyTo = replyTo - self.cc = cc?.joined(separator: ",") - self.bcc = bcc?.joined(separator: ",") - self.subject = subject - self.text = text - self.html = html - self.attachment = attachments - self.inline = inline - } - - public init(from: String, to: [FullEmail], replyTo: String? = nil, cc: [FullEmail]? = nil, bcc: [FullEmail]? = nil, subject: String, text: String, html: String? = nil, attachments: [File]? = nil, inline: [File]? = nil) { - self.from = from - self.to = to.stringArray.joined(separator: ",") - self.replyTo = replyTo - self.cc = cc?.stringArray.joined(separator: ",") - self.bcc = bcc?.stringArray.joined(separator: ",") - self.subject = subject - self.text = text - self.html = html - self.attachment = attachments - self.inline = inline - } +public struct MailgunMessage: Content { + public static var defaultContentType: HTTPMediaType = .formData + + public typealias FullEmail = (email: String, name: String?) + + public let from: String + public let to: String + public let replyTo: String? + public let cc: String? + public let bcc: String? + public let subject: String + public let text: String + public let html: String? + public let attachment: [File]? + public let inline: [File]? + + private enum CodingKeys: String, CodingKey { + case from + case to + case replyTo = "h:Reply-To" + case cc + case bcc + case subject + case text + case html + case attachment + case inline + } + + public init(from: String, to: String, replyTo: String? = nil, cc: String? = nil, bcc: String? = nil, subject: String, text: String, html: String? = nil, attachments: [File]? = nil, inline: [File]? = nil) { + self.from = from + self.to = to + self.replyTo = replyTo + self.cc = cc + self.bcc = bcc + self.subject = subject + self.text = text + self.html = html + self.attachment = attachments + self.inline = inline + } + + public init(from: String, to: [String], replyTo: String? = nil, cc: [String]? = nil, bcc: [String]? = nil, subject: String, text: String, html: String? = nil, attachments: [File]? = nil, inline: [File]? = nil) { + self.from = from + self.to = to.joined(separator: ",") + self.replyTo = replyTo + self.cc = cc?.joined(separator: ",") + self.bcc = bcc?.joined(separator: ",") + self.subject = subject + self.text = text + self.html = html + self.attachment = attachments + self.inline = inline + } + + public init(from: String, to: [FullEmail], replyTo: String? = nil, cc: [FullEmail]? = nil, bcc: [FullEmail]? = nil, subject: String, text: String, html: String? = nil, attachments: [File]? = nil, inline: [File]? = nil) { + self.from = from + self.to = to.stringArray.joined(separator: ",") + self.replyTo = replyTo + self.cc = cc?.stringArray.joined(separator: ",") + self.bcc = bcc?.stringArray.joined(separator: ",") + self.subject = subject + self.text = text + self.html = html + self.attachment = attachments + self.inline = inline } } diff --git a/Sources/Mailgun/Models/RouteSetup.swift b/Sources/Mailgun/Models/RouteSetup.swift index 2f54e6c..cc56617 100644 --- a/Sources/Mailgun/Models/RouteSetup.swift +++ b/Sources/Mailgun/Models/RouteSetup.swift @@ -1,8 +1,8 @@ import Vapor import Foundation -public struct RouteSetup: Content { - public static var defaultContentType: MediaType = .urlEncodedForm +public struct MailgunRouteSetup: Content { + public static var defaultContentType: HTTPMediaType = .urlEncodedForm public let priority: Int public let description: String diff --git a/Sources/Mailgun/Models/Template.swift b/Sources/Mailgun/Models/Template.swift index 26e7e5d..5e9c1c3 100644 --- a/Sources/Mailgun/Models/Template.swift +++ b/Sources/Mailgun/Models/Template.swift @@ -1,34 +1,32 @@ import Vapor -extension Mailgun { - /// Template, see https://documentation.mailgun.com/en/latest/api-templates.html#templates - public struct Template: Content { - public static var defaultContentType: MediaType = .formData +/// Template, see https://documentation.mailgun.com/en/latest/api-templates.html#templates +public struct MailgunTemplate: Content { + public static var defaultContentType: HTTPMediaType = .formData - public let name: String - public let description: String - public let template: String? - public let tag: String? - public let engine: String? - public let versionComment: String? - - private enum CodingKeys: String, CodingKey { - case name - case description - case template - case tag - case engine - case versionComment = "comment" - } - - public init(name: String, description: String, template: String? = nil, tag: String? = nil, engine: String? = nil, versionComment: String? = nil) { - self.name = name - self.description = description - self.template = template - self.tag = tag - self.engine = engine - self.versionComment = versionComment - } + public let name: String + public let description: String + public let template: String? + public let tag: String? + public let engine: String? + public let versionComment: String? + + private enum CodingKeys: String, CodingKey { + case name + case description + case template + case tag + case engine + case versionComment = "comment" + } + + public init(name: String, description: String, template: String? = nil, tag: String? = nil, engine: String? = nil, versionComment: String? = nil) { + self.name = name + self.description = description + self.template = template + self.tag = tag + self.engine = engine + self.versionComment = versionComment } } diff --git a/Sources/Mailgun/Models/TemplateMessage.swift b/Sources/Mailgun/Models/TemplateMessage.swift index 254c2fd..16befdb 100644 --- a/Sources/Mailgun/Models/TemplateMessage.swift +++ b/Sources/Mailgun/Models/TemplateMessage.swift @@ -1,103 +1,101 @@ import Vapor -extension Mailgun { - public struct TemplateMessage: Content { - public static var defaultContentType: MediaType = .formData - - public typealias FullEmail = (email: String, name: String?) - - public let from: String - public let to: String - public let replyTo: String? - public let cc: String? - public let bcc: String? - public let subject: String - public let template: String - public let templateData: [String:String]? - public let templateVersion: String? - public let templateText: Bool? - public let attachment: [File]? - public let inline: [File]? - - private enum CodingKeys: String, CodingKey { - case from - case to - case replyTo = "h:Reply-To" - case cc - case bcc - case subject - case template - case attachment - case inline - case templateData = "h:X-Mailgun-Variables" - case templateVersion = "t:version" - case templateText = "t:text" - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(from, forKey: .from) - try container.encode(to, forKey: .to) - try container.encode(cc, forKey: .cc) - try container.encode(bcc, forKey: .bcc) - try container.encode(subject, forKey: .subject) - try container.encode(template, forKey: .template) - if let templateData = templateData { - guard let jsonData = try? JSONEncoder().encode(templateData), let jsonString = String(data: jsonData, encoding: .utf8) else { - throw Error.encodingProblem - } - try container.encode(jsonString, forKey: .templateData) - } - try container.encode(templateVersion, forKey: .templateVersion) - let text = templateText == true ? "yes" : nil // need to send yes as string - try container.encode(text, forKey: .templateText) - try container.encode(attachment, forKey: .attachment) - try container.encode(inline, forKey: .inline) - } - - public init(from: String, to: String, replyTo: String? = nil, cc: String? = nil, bcc: String? = nil, subject: String, template: String, templateData: [String:String]? = nil, templateVersion: String? = nil, templateText: Bool? = nil, attachments: [File]? = nil, inline: [File]? = nil) { - self.from = from - self.to = to - self.replyTo = replyTo - self.cc = cc - self.bcc = bcc - self.subject = subject - self.template = template - self.templateData = templateData - self.templateVersion = templateVersion - self.templateText = templateText - self.attachment = attachments - self.inline = inline - } - - public init(from: String, to: [String], replyTo: String? = nil, cc: [String]? = nil, bcc: [String]? = nil, subject: String, template: String, templateData: [String:String]? = nil, templateVersion: String? = nil, templateText: Bool? = nil, attachments: [File]? = nil, inline: [File]? = nil) { - self.from = from - self.to = to.joined(separator: ",") - self.replyTo = replyTo - self.cc = cc?.joined(separator: ",") - self.bcc = bcc?.joined(separator: ",") - self.subject = subject - self.template = template - self.templateData = templateData - self.templateVersion = templateVersion - self.templateText = templateText - self.attachment = attachments - self.inline = inline - } - - public init(from: String, to: [FullEmail], replyTo: String? = nil, cc: [FullEmail]? = nil, bcc: [FullEmail]? = nil, subject: String, template: String, templateData: [String:String]? = nil, templateVersion: String? = nil, templateText: Bool? = nil, attachments: [File]? = nil, inline: [File]? = nil) { - self.from = from - self.to = to.stringArray.joined(separator: ",") - self.replyTo = replyTo - self.cc = cc?.stringArray.joined(separator: ",") - self.bcc = bcc?.stringArray.joined(separator: ",") - self.subject = subject - self.template = template - self.templateData = templateData - self.templateVersion = templateVersion - self.templateText = templateText - self.attachment = attachments - self.inline = inline +public struct MailgunTemplateMessage: Content { + public static var defaultContentType: HTTPMediaType = .formData + + public typealias FullEmail = (email: String, name: String?) + + public let from: String + public let to: String + public let replyTo: String? + public let cc: String? + public let bcc: String? + public let subject: String + public let template: String + public let templateData: [String:String]? + public let templateVersion: String? + public let templateText: Bool? + public let attachment: [File]? + public let inline: [File]? + + private enum CodingKeys: String, CodingKey { + case from + case to + case replyTo = "h:Reply-To" + case cc + case bcc + case subject + case template + case attachment + case inline + case templateData = "h:X-Mailgun-Variables" + case templateVersion = "t:version" + case templateText = "t:text" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + try container.encode(cc, forKey: .cc) + try container.encode(bcc, forKey: .bcc) + try container.encode(subject, forKey: .subject) + try container.encode(template, forKey: .template) + if let templateData = templateData { + guard let jsonData = try? JSONEncoder().encode(templateData), + let jsonString = String(data: jsonData, encoding: .utf8) + else { throw MailgunError.encodingProblem } + try container.encode(jsonString, forKey: .templateData) } + try container.encode(templateVersion, forKey: .templateVersion) + let text = templateText == true ? "yes" : nil // need to send yes as string + try container.encode(text, forKey: .templateText) + try container.encode(attachment, forKey: .attachment) + try container.encode(inline, forKey: .inline) + } + + public init(from: String, to: String, replyTo: String? = nil, cc: String? = nil, bcc: String? = nil, subject: String, template: String, templateData: [String:String]? = nil, templateVersion: String? = nil, templateText: Bool? = nil, attachments: [File]? = nil, inline: [File]? = nil) { + self.from = from + self.to = to + self.replyTo = replyTo + self.cc = cc + self.bcc = bcc + self.subject = subject + self.template = template + self.templateData = templateData + self.templateVersion = templateVersion + self.templateText = templateText + self.attachment = attachments + self.inline = inline + } + + public init(from: String, to: [String], replyTo: String? = nil, cc: [String]? = nil, bcc: [String]? = nil, subject: String, template: String, templateData: [String:String]? = nil, templateVersion: String? = nil, templateText: Bool? = nil, attachments: [File]? = nil, inline: [File]? = nil) { + self.from = from + self.to = to.joined(separator: ",") + self.replyTo = replyTo + self.cc = cc?.joined(separator: ",") + self.bcc = bcc?.joined(separator: ",") + self.subject = subject + self.template = template + self.templateData = templateData + self.templateVersion = templateVersion + self.templateText = templateText + self.attachment = attachments + self.inline = inline + } + + public init(from: String, to: [FullEmail], replyTo: String? = nil, cc: [FullEmail]? = nil, bcc: [FullEmail]? = nil, subject: String, template: String, templateData: [String:String]? = nil, templateVersion: String? = nil, templateText: Bool? = nil, attachments: [File]? = nil, inline: [File]? = nil) { + self.from = from + self.to = to.stringArray.joined(separator: ",") + self.replyTo = replyTo + self.cc = cc?.stringArray.joined(separator: ",") + self.bcc = bcc?.stringArray.joined(separator: ",") + self.subject = subject + self.template = template + self.templateData = templateData + self.templateVersion = templateVersion + self.templateText = templateText + self.attachment = attachments + self.inline = inline } -} \ No newline at end of file +} diff --git a/Tests/MailgunTests/MailgunTests.swift b/Tests/MailgunTests/MailgunTests.swift index ba3e4ed..a04980d 100644 --- a/Tests/MailgunTests/MailgunTests.swift +++ b/Tests/MailgunTests/MailgunTests.swift @@ -6,7 +6,7 @@ final class MailgunTests: XCTestCase { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - XCTAssertEqual(Mailgun().text, "Hello, World!") +// XCTAssertEqual(Mailgun().text, "Hello, World!") } From a8b61c65a27c48008eebaf182e5c73511b0eaed0 Mon Sep 17 00:00:00 2001 From: iMike Date: Tue, 14 Jan 2020 01:48:45 +0400 Subject: [PATCH 02/15] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d474a00..d790505 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![Slack](https://img.shields.io/badge/join-slack-745EAF.svg?style=flat)](https://vapor.team) [![Platforms](https://img.shields.io/badge/platforms-macOS%2010.13%20|%20Ubuntu%2016.04%20LTS-ff0000.svg?style=flat)](http://cocoapods.org/pods/FASwift) -[![Swift 5.1](https://img.shields.io/badge/swift-4.1-orange.svg?style=flat)](http://swift.org) -[![Vapor 4](https://img.shields.io/badge/vapor-3.0-blue.svg?style=flat)](https://vapor.codes) +[![Swift 5.1](https://img.shields.io/badge/swift-5.1-orange.svg?style=flat)](http://swift.org) +[![Vapor 4](https://img.shields.io/badge/vapor-4.0-blue.svg?style=flat)](https://vapor.codes) ## From 8dacf9d073a50ef306179b91582893fe86a26766 Mon Sep 17 00:00:00 2001 From: iMike Date: Tue, 14 Jan 2020 02:30:31 +0400 Subject: [PATCH 03/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d790505..75a97ff 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Make sure you get an API key and register a custom domain In `configure.swift`: ```swift -import FCM +import Mailgun // Called before your application initializes. func configure(_ app: Application) throws { From 6bde823e138ae86db821146433b40b270a8b3808 Mon Sep 17 00:00:00 2001 From: iMike Date: Tue, 14 Jan 2020 02:31:22 +0400 Subject: [PATCH 04/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 75a97ff..c1a1465 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ func configure(_ app: Application) throws { /// MAILGUN_API_KEY=... /// MAILGUN_DOMAIN=... /// MAILGUN_REGION=... - app.fcm.configuration = .environment + app.mailgun.configuration = .environment /// case 2 /// manually From 99b19b6fcc06b8fbe3e7c7cc503780d858947ea3 Mon Sep 17 00:00:00 2001 From: iMike Date: Sun, 19 Jan 2020 03:57:30 +0400 Subject: [PATCH 05/15] Drop platform to `.macOS(.v10_14)` --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index ab853cf..3c709f2 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "Mailgun", platforms: [ - .macOS(.v10_15) + .macOS(.v10_14) ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. From 66ff21e60443f4652519a74d19672c87187ca315 Mon Sep 17 00:00:00 2001 From: Mihael Isaev Date: Sun, 19 Jan 2020 04:18:55 +0400 Subject: [PATCH 06/15] Implement multiple domains --- README.md | 27 ++++++++++++------ Sources/Mailgun/Mailgun.swift | 20 ++++++++++---- Sources/Mailgun/Models/Configuration.swift | 32 +++------------------- Sources/Mailgun/Models/Domain.swift | 9 ++++++ 4 files changed, 47 insertions(+), 41 deletions(-) create mode 100644 Sources/Mailgun/Models/Domain.swift diff --git a/README.md b/README.md index c1a1465..84db1e6 100644 --- a/README.md +++ b/README.md @@ -38,21 +38,32 @@ func configure(_ app: Application) throws { /// case 1 /// put into your environment variables the following keys: /// MAILGUN_API_KEY=... - /// MAILGUN_DOMAIN=... - /// MAILGUN_REGION=... app.mailgun.configuration = .environment /// case 2 /// manually - app.mailgun.configuration = .init(apiKey: "", domain: "mg.example.com", region: .eu) + app.mailgun.configuration = .init(apiKey: "") } ``` +### Declare all your domains + +```swift +extension MailgunDomain { + static var myApp1: MailgunDomain { .init("mg.myapp1.com", .us) } + static var myApp2: MailgunDomain { .init("mg.myapp2.com", .eu) } + static var myApp3: MailgunDomain { .init("mg.myapp3.com", .us) } + static var myApp4: MailgunDomain { .init("mg.myapp4.com", .eu) } +} +``` + > Note: If your private api key begins with `key-`, be sure to include it ### Use +> Note: you could call `.mailgun(.myApp1)` from both `Application` and `Request` + In `routes.swift`: #### Without attachments @@ -69,7 +80,7 @@ func routes(_ app: Application) throws { text: "This is a newsletter", html: "

This is a newsletter

" ) - return req.mailgun.send(message) + return req.mailgun(.myApp1).send(message) } } ``` @@ -97,7 +108,7 @@ func routes(_ app: Application) throws { html: "

This is a newsletter

", attachments: [attachment] ) - return req.mailgun.send(message) + return req.mailgun(.myApp1).send(message) } } ``` @@ -116,7 +127,7 @@ func routes(_ app: Application) throws { template: "my-template", templateData: ["foo": "bar"] ) - return req.mailgun.send(message) + return req.mailgun(.myApp1).send(message) } } ``` @@ -154,7 +165,7 @@ func routes(_ app: Application) throws { html: content ) - return req.mailgun.send(message) + return req.mailgun(.myApp1).send(message) } } ``` @@ -199,7 +210,7 @@ func routes(_ app: Application) throws { let mailgunGroup = app.grouped("mailgun") mailgunGroup.post("template") { req -> EventLoopFuture in let template = MailgunTemplate(name: "my-template", description: "api created :)", template: "

Hello {{ name }}

") - return req.mailgun.createTemplate(template) + return req.mailgun(.myApp1).createTemplate(template) } } ``` diff --git a/Sources/Mailgun/Mailgun.swift b/Sources/Mailgun/Mailgun.swift index 0897305..b5c7f2c 100644 --- a/Sources/Mailgun/Mailgun.swift +++ b/Sources/Mailgun/Mailgun.swift @@ -14,11 +14,13 @@ public protocol MailgunProvider { public struct Mailgun: MailgunProvider { let application: Application + let domain: MailgunDomain // MARK: Initialization - public init (_ app: Application) { - application = app + public init (_ application: Application, _ domain: MailgunDomain) { + self.application = application + self.domain = domain } } @@ -42,6 +44,14 @@ extension Mailgun { // MARK: - Send message extension Mailgun { + /// Base API URL based on the current region + var baseApiUrl: String { + switch domain.region { + case .us: return "https://api.mailgun.net/v3" + case .eu: return "https://api.eu.mailgun.net/v3" + } + } + /// Send message /// /// - Parameters: @@ -84,11 +94,11 @@ extension Mailgun { } extension Application { - public var mailgun: Mailgun { .init(self) } + public func mailgun(_ domain: MailgunDomain) -> Mailgun { .init(self, domain) } } extension Request { - public var mailgun: Mailgun { .init(application) } + public func mailgun(_ domain: MailgunDomain) -> Mailgun { .init(application, domain) } } // MARK: - Private @@ -116,7 +126,7 @@ fileprivate extension Mailgun { headers.add(name: .authorization, value: "Basic \(authKeyEncoded)") return headers }.flatMap { headers in - let mailgunURI = URI(string: "\(configuration.baseApiUrl)/\(configuration.domain)/\(endpoint)") + let mailgunURI = URI(string: "\(self.baseApiUrl)/\(self.domain.domain)/\(endpoint)") return self.application.client.post(mailgunURI, headers: headers) { req in try req.content.encode(content) }.flatMapThrowing { diff --git a/Sources/Mailgun/Models/Configuration.swift b/Sources/Mailgun/Models/Configuration.swift index c1ee942..21d3ad8 100644 --- a/Sources/Mailgun/Models/Configuration.swift +++ b/Sources/Mailgun/Models/Configuration.swift @@ -5,45 +5,21 @@ public struct MailgunConfiguration { /// API key (including "key-" prefix) public let apiKey: String - /// Domain - public let domain: String - - /// Region - public let region: MailgunRegion - /// Initializer /// /// - Parameters: /// - apiKey: API key including "key-" prefix /// - domain: API domain - public init(apiKey: String, domain: String, region: MailgunRegion) { + public init(apiKey: String) { self.apiKey = apiKey - self.domain = domain - self.region = region - } - - var baseApiUrl: String { - switch region { - case .us: return "https://api.mailgun.net/v3" - case .eu: return "https://api.eu.mailgun.net/v3" - } } /// It will try to initialize configuration with environment variables: - /// - MG_KEY - /// - MG_DOMAIN - /// - MG_REGION + /// - MAILGUN_API_KEY public static var environment: MailgunConfiguration { - guard - let apiKey = Environment.get("MAILGUN_API_KEY"), - let domain = Environment.get("MAILGUN_DOMAIN"), - let rawRegion = Environment.get("MAILGUN_REGION") - else { + guard let apiKey = Environment.get("MAILGUN_API_KEY") else { fatalError("Mailgun environmant variables not set") } - guard let region = MailgunRegion(rawValue: rawRegion.lowercased()) else { - fatalError("Mailgun unable to parse environmant region value") - } - return .init(apiKey: apiKey, domain: domain, region: region) + return .init(apiKey: apiKey) } } diff --git a/Sources/Mailgun/Models/Domain.swift b/Sources/Mailgun/Models/Domain.swift new file mode 100644 index 0000000..194c13a --- /dev/null +++ b/Sources/Mailgun/Models/Domain.swift @@ -0,0 +1,9 @@ +public struct MailgunDomain { + public let domain: String + public let region: MailgunRegion + + public init(_ domain: String, _ region: MailgunRegion) { + self.domain = domain + self.region = region + } +} From f3336bd8ea8451a580246ed86eabd82cc9a93dc3 Mon Sep 17 00:00:00 2001 From: Mihael Isaev Date: Sun, 19 Jan 2020 04:32:27 +0400 Subject: [PATCH 07/15] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 84db1e6..1e4fdc6 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ ## `Mailgun` is a Vapor 4 service for a popular [email sending API](https://www.mailgun.com/) -> Note: Vapor3 version is available in `vapor3` branch and from `1.5.0` tag +> Note: Vapor3 version is available in `vapor3` branch and from `3.0.0` tag ## Installation Vapor Mailgun Service can be installed with Swift Package Manager ```swift -.package(url: "https://github.com/twof/VaporMailgunService.git", from: "2.0.0") +.package(url: "https://github.com/twof/VaporMailgunService.git", from: "4.0.0") //and in targets add //"Mailgun" From 71b063f0b161a1bc92e3cead22b617b9ef36c7e7 Mon Sep 17 00:00:00 2001 From: iMike Date: Sun, 19 Jan 2020 04:39:21 +0400 Subject: [PATCH 08/15] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1e4fdc6..8a8c624 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ func configure(_ app: Application) throws { } ``` +> Note: If your private api key begins with `key-`, be sure to include it + ### Declare all your domains ```swift @@ -57,9 +59,6 @@ extension MailgunDomain { } ``` - -> Note: If your private api key begins with `key-`, be sure to include it - ### Use > Note: you could call `.mailgun(.myApp1)` from both `Application` and `Request` From f9c476fbdf58033ac447908bdc028fac1408fcc3 Mon Sep 17 00:00:00 2001 From: Mihael Isaev Date: Sun, 19 Jan 2020 05:18:38 +0400 Subject: [PATCH 09/15] Move configuration to `app.mailgunConfiguration` since it is not cool to set global configuration through `app.mailgun(.myApp1).configuration` --- README.md | 4 ++-- Sources/Mailgun/Mailgun.swift | 23 ++++++++++++++++------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8a8c624..88c7b30 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,11 @@ func configure(_ app: Application) throws { /// case 1 /// put into your environment variables the following keys: /// MAILGUN_API_KEY=... - app.mailgun.configuration = .environment + app.mailgunConfiguration = .environment /// case 2 /// manually - app.mailgun.configuration = .init(apiKey: "") + app.mailgunConfiguration = .init(apiKey: "") } ``` diff --git a/Sources/Mailgun/Mailgun.swift b/Sources/Mailgun/Mailgun.swift index b5c7f2c..afe681d 100644 --- a/Sources/Mailgun/Mailgun.swift +++ b/Sources/Mailgun/Mailgun.swift @@ -5,14 +5,18 @@ import Foundation // MARK: - Service public protocol MailgunProvider { - var configuration: MailgunConfiguration? { get set } func send(_ content: MailgunMessage) throws -> EventLoopFuture func send(_ content: MailgunTemplateMessage) throws -> EventLoopFuture func setup(forwarding: MailgunRouteSetup) throws -> EventLoopFuture func createTemplate(_ template: MailgunTemplate) throws -> EventLoopFuture } -public struct Mailgun: MailgunProvider { +internal protocol _MailgunProvider: MailgunProvider { + var application: Application { get } + var configuration: MailgunConfiguration? { get } +} + +public struct Mailgun: _MailgunProvider { let application: Application let domain: MailgunDomain @@ -26,17 +30,22 @@ public struct Mailgun: MailgunProvider { // MARK: - Configuration -extension Mailgun { +extension _MailgunProvider { + var configuration: MailgunConfiguration? { application.mailgunConfiguration } +} + +extension Application { struct ConfigurationKey: StorageKey { typealias Value = MailgunConfiguration } - public var configuration: MailgunConfiguration? { + /// Global Mailgun configuration for all the domains + public var mailgunConfiguration: MailgunConfiguration? { get { - application.storage[ConfigurationKey.self] + storage[ConfigurationKey.self] } - nonmutating set { - application.storage[ConfigurationKey.self] = newValue + set { + storage[ConfigurationKey.self] = newValue } } } From 2e3eb3e39ca490531628a4b6f34a005f629d8722 Mon Sep 17 00:00:00 2001 From: Mihael Isaev Date: Sun, 19 Jan 2020 16:32:29 +0400 Subject: [PATCH 10/15] Implement `storage`, `defaultDomain`. Refactoring. --- README.md | 54 +++++++++--- .../Extensions/Mailgun+Application.swift | 20 +++++ .../Mailgun/Extensions/Mailgun+Encode.swift | 12 +++ .../Extensions/Mailgun+ParseResponse.swift | 21 +++++ .../Extensions/Mailgun+PostRequest.swift | 23 +++++ .../Mailgun/Extensions/Mailgun+Request.swift | 11 +++ Sources/Mailgun/Mailgun.swift | 88 +------------------ Sources/Mailgun/Storage.swift | 37 ++++++++ 8 files changed, 171 insertions(+), 95 deletions(-) create mode 100644 Sources/Mailgun/Extensions/Mailgun+Application.swift create mode 100644 Sources/Mailgun/Extensions/Mailgun+Encode.swift create mode 100644 Sources/Mailgun/Extensions/Mailgun+ParseResponse.swift create mode 100644 Sources/Mailgun/Extensions/Mailgun+PostRequest.swift create mode 100644 Sources/Mailgun/Extensions/Mailgun+Request.swift create mode 100644 Sources/Mailgun/Storage.swift diff --git a/README.md b/README.md index 88c7b30..f81893c 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,11 @@ func configure(_ app: Application) throws { /// case 1 /// put into your environment variables the following keys: /// MAILGUN_API_KEY=... - app.mailgunConfiguration = .environment + app.mailgun.configuration = .environment /// case 2 /// manually - app.mailgunConfiguration = .init(apiKey: "") + app.mailgun.configuration = .init(apiKey: "") } ``` @@ -59,9 +59,43 @@ extension MailgunDomain { } ``` -### Use +Set default domain in `configure.swift` -> Note: you could call `.mailgun(.myApp1)` from both `Application` and `Request` +```swift +app.mailgun.defaultDomain = .myApp1 +``` + +### Usage + +`Mailgun` is available on both `Application` and `Request` + +```swift +// call it without arguments to use default domain +app.mailgun().send(...) +req.mailgun().send(...) + +// or call it with domain +app.mailgun(.myApp1).send(...) +app.mailgun(.myApp1).send(...) +``` + +In `configure.swift` + +```swift +import Mailgun + +// Called before your application initializes. +func configure(_ app: Application) throws { + /// configure mailgun + + /// then you're ready to use it + app.mailgun(.myApp1).send(...).whenSuccess { response in + print("just sent: \(response)") + } +} +``` + +All the examples below will be with `Request`, but you could do the same with `Application` as in example above. In `routes.swift`: @@ -79,7 +113,7 @@ func routes(_ app: Application) throws { text: "This is a newsletter", html: "

This is a newsletter

" ) - return req.mailgun(.myApp1).send(message) + return req.mailgun().send(message) } } ``` @@ -107,7 +141,7 @@ func routes(_ app: Application) throws { html: "

This is a newsletter

", attachments: [attachment] ) - return req.mailgun(.myApp1).send(message) + return req.mailgun().send(message) } } ``` @@ -126,7 +160,7 @@ func routes(_ app: Application) throws { template: "my-template", templateData: ["foo": "bar"] ) - return req.mailgun(.myApp1).send(message) + return req.mailgun().send(message) } } ``` @@ -164,7 +198,7 @@ func routes(_ app: Application) throws { html: content ) - return req.mailgun(.myApp1).send(message) + return req.mailgun().send(message) } } ``` @@ -175,7 +209,7 @@ func routes(_ app: Application) throws { public func configure(_ app: Application) throws { // sets up a catch_all forward for the route listed let routeSetup = MailgunRouteSetup(forwardURL: "http://example.com/mailgun/all", description: "A route for all emails") - try app.mailgun.setup(forwarding: routeSetup).map { response in + app.mailgun().setup(forwarding: routeSetup).whenSuccess { response in print(response) } } @@ -209,7 +243,7 @@ func routes(_ app: Application) throws { let mailgunGroup = app.grouped("mailgun") mailgunGroup.post("template") { req -> EventLoopFuture in let template = MailgunTemplate(name: "my-template", description: "api created :)", template: "

Hello {{ name }}

") - return req.mailgun(.myApp1).createTemplate(template) + return req.mailgun().createTemplate(template) } } ``` diff --git a/Sources/Mailgun/Extensions/Mailgun+Application.swift b/Sources/Mailgun/Extensions/Mailgun+Application.swift new file mode 100644 index 0000000..9952ebd --- /dev/null +++ b/Sources/Mailgun/Extensions/Mailgun+Application.swift @@ -0,0 +1,20 @@ +import Vapor + +extension Application { + public var mailgun: MailgunStorage { + .init(self) + } + + /// Mailgun with selected or default domain. + /// Default domain should be configured in advance through `app.mailgun.defaultDomain` + public func mailgun(_ domain: MailgunDomain? = nil) -> Mailgun { + if let domain = domain { + return .init(self, domain) + } + let storage = MailgunStorage(self) + guard let defaultDomain = storage.defaultDomain else { + fatalError("Mailgun default domain not configured. Use app.mailgun.defaultDomain = ...") + } + return .init(self, defaultDomain) + } +} diff --git a/Sources/Mailgun/Extensions/Mailgun+Encode.swift b/Sources/Mailgun/Extensions/Mailgun+Encode.swift new file mode 100644 index 0000000..59ce8c9 --- /dev/null +++ b/Sources/Mailgun/Extensions/Mailgun+Encode.swift @@ -0,0 +1,12 @@ +extension Mailgun { + func encode(apiKey: String) throws -> String { + guard let apiKeyData = "api:\(apiKey)".data(using: .utf8) else { + throw MailgunError.encodingProblem + } + let authKey = apiKeyData.base64EncodedData() + guard let authKeyEncoded = String.init(data: authKey, encoding: .utf8) else { + throw MailgunError.encodingProblem + } + return authKeyEncoded + } +} diff --git a/Sources/Mailgun/Extensions/Mailgun+ParseResponse.swift b/Sources/Mailgun/Extensions/Mailgun+ParseResponse.swift new file mode 100644 index 0000000..0238c3b --- /dev/null +++ b/Sources/Mailgun/Extensions/Mailgun+ParseResponse.swift @@ -0,0 +1,21 @@ +import Vapor + +extension Mailgun { + func parse(response: ClientResponse) throws -> ClientResponse { + switch true { + case response.status == .ok: + return response + case response.status == .unauthorized: + throw MailgunError.authenticationFailed + default: + if let body = response.body, let err = try? JSONDecoder().decode(MailgunErrorResponse.self, from: body) { + if err.message.hasPrefix("template") { + throw MailgunError.unableToCreateTemplate(err) + } else { + throw MailgunError.unableToSendEmail(err) + } + } + throw MailgunError.unknownError(response) + } + } +} diff --git a/Sources/Mailgun/Extensions/Mailgun+PostRequest.swift b/Sources/Mailgun/Extensions/Mailgun+PostRequest.swift new file mode 100644 index 0000000..dca25e6 --- /dev/null +++ b/Sources/Mailgun/Extensions/Mailgun+PostRequest.swift @@ -0,0 +1,23 @@ +import Vapor + +extension Mailgun { + func postRequest(_ content: Message, endpoint: String) -> EventLoopFuture { + guard let configuration = self.storage.configuration else { + fatalError("Mailgun not configured. Use app.mailgun.configuration = ...") + } + + return application.eventLoopGroup.future().flatMapThrowing { _ -> HTTPHeaders in + let authKeyEncoded = try self.encode(apiKey: configuration.apiKey) + var headers = HTTPHeaders() + headers.add(name: .authorization, value: "Basic \(authKeyEncoded)") + return headers + }.flatMap { headers in + let mailgunURI = URI(string: "\(self.baseApiUrl)/\(self.domain.domain)/\(endpoint)") + return self.application.client.post(mailgunURI, headers: headers) { req in + try req.content.encode(content) + }.flatMapThrowing { + try self.parse(response: $0) + } + } + } +} diff --git a/Sources/Mailgun/Extensions/Mailgun+Request.swift b/Sources/Mailgun/Extensions/Mailgun+Request.swift new file mode 100644 index 0000000..e4c9aee --- /dev/null +++ b/Sources/Mailgun/Extensions/Mailgun+Request.swift @@ -0,0 +1,11 @@ +import Vapor + +extension Request { + public var mailgun: MailgunStorage { + application.mailgun + } + + public func mailgun(_ domain: MailgunDomain? = nil) -> Mailgun { + application.mailgun(domain) + } +} diff --git a/Sources/Mailgun/Mailgun.swift b/Sources/Mailgun/Mailgun.swift index afe681d..c018530 100644 --- a/Sources/Mailgun/Mailgun.swift +++ b/Sources/Mailgun/Mailgun.swift @@ -1,7 +1,6 @@ import Vapor import Foundation - // MARK: - Service public protocol MailgunProvider { @@ -13,40 +12,20 @@ public protocol MailgunProvider { internal protocol _MailgunProvider: MailgunProvider { var application: Application { get } - var configuration: MailgunConfiguration? { get } + var storage: MailgunStorage { get } } public struct Mailgun: _MailgunProvider { let application: Application let domain: MailgunDomain + let storage: MailgunStorage // MARK: Initialization public init (_ application: Application, _ domain: MailgunDomain) { self.application = application self.domain = domain - } -} - -// MARK: - Configuration - -extension _MailgunProvider { - var configuration: MailgunConfiguration? { application.mailgunConfiguration } -} - -extension Application { - struct ConfigurationKey: StorageKey { - typealias Value = MailgunConfiguration - } - - /// Global Mailgun configuration for all the domains - public var mailgunConfiguration: MailgunConfiguration? { - get { - storage[ConfigurationKey.self] - } - set { - storage[ConfigurationKey.self] = newValue - } + self.storage = MailgunStorage(application) } } @@ -102,67 +81,6 @@ extension Mailgun { } } -extension Application { - public func mailgun(_ domain: MailgunDomain) -> Mailgun { .init(self, domain) } -} - -extension Request { - public func mailgun(_ domain: MailgunDomain) -> Mailgun { .init(application, domain) } -} - -// MARK: - Private - -fileprivate extension Mailgun { - func encode(apiKey: String) throws -> String { - guard let apiKeyData = "api:\(apiKey)".data(using: .utf8) else { - throw MailgunError.encodingProblem - } - let authKey = apiKeyData.base64EncodedData() - guard let authKeyEncoded = String.init(data: authKey, encoding: .utf8) else { - throw MailgunError.encodingProblem - } - return authKeyEncoded - } - - private func postRequest(_ content: Message, endpoint: String) -> EventLoopFuture { - guard let configuration = self.configuration else { - fatalError("Mailgun not configured. Use app.mailgun.configuration = ...") - } - - return application.eventLoopGroup.future().flatMapThrowing { _ -> HTTPHeaders in - let authKeyEncoded = try self.encode(apiKey: configuration.apiKey) - var headers = HTTPHeaders() - headers.add(name: .authorization, value: "Basic \(authKeyEncoded)") - return headers - }.flatMap { headers in - let mailgunURI = URI(string: "\(self.baseApiUrl)/\(self.domain.domain)/\(endpoint)") - return self.application.client.post(mailgunURI, headers: headers) { req in - try req.content.encode(content) - }.flatMapThrowing { - try self.process($0) - } - } - } - - private func process(_ response: ClientResponse) throws -> ClientResponse { - switch true { - case response.status == .ok: - return response - case response.status == .unauthorized: - throw MailgunError.authenticationFailed - default: - if let body = response.body, let err = try? JSONDecoder().decode(MailgunErrorResponse.self, from: body) { - if err.message.hasPrefix("template") { - throw MailgunError.unableToCreateTemplate(err) - } else { - throw MailgunError.unableToSendEmail(err) - } - } - throw MailgunError.unknownError(response) - } - } -} - // MARK: - Conversions extension Array where Element == MailgunMessage.FullEmail { diff --git a/Sources/Mailgun/Storage.swift b/Sources/Mailgun/Storage.swift new file mode 100644 index 0000000..178417e --- /dev/null +++ b/Sources/Mailgun/Storage.swift @@ -0,0 +1,37 @@ +import Vapor + +public class MailgunStorage { + let application: Application + + init (_ application: Application) { + self.application = application + } + + struct ConfigurationKey: StorageKey { + typealias Value = MailgunConfiguration + } + + /// Global Mailgun configuration for all the domains + public var configuration: MailgunConfiguration? { + get { + application.storage[ConfigurationKey.self] + } + set { + application.storage[ConfigurationKey.self] = newValue + } + } + + struct DefaultDomainKey: StorageKey { + typealias Value = MailgunDomain + } + + /// Default mailgun domain which will be used when you call `app.mailgun.` + public var defaultDomain: MailgunDomain? { + get { + application.storage[DefaultDomainKey.self] + } + set { + application.storage[DefaultDomainKey.self] = newValue + } + } +} From 72d523c5dec300b79848c9e5a493684e1dc0c974 Mon Sep 17 00:00:00 2001 From: iMike Date: Sun, 19 Jan 2020 16:35:15 +0400 Subject: [PATCH 11/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f81893c..c4a7413 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ req.mailgun().send(...) // or call it with domain app.mailgun(.myApp1).send(...) -app.mailgun(.myApp1).send(...) +req.mailgun(.myApp1).send(...) ``` In `configure.swift` From c6557c9719fec5a988edee4aacdaacd161268eed Mon Sep 17 00:00:00 2001 From: iMike Date: Sun, 19 Jan 2020 16:37:47 +0400 Subject: [PATCH 12/15] Update README.md --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c4a7413..110ed23 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ app.mailgun(.myApp1).send(...) req.mailgun(.myApp1).send(...) ``` -In `configure.swift` +#### In `configure.swift` ```swift import Mailgun @@ -95,11 +95,11 @@ func configure(_ app: Application) throws { } ``` -All the examples below will be with `Request`, but you could do the same with `Application` as in example above. +> 💡 NOTE: All the examples below will be with `Request`, but you could do the same with `Application` as in example above. -In `routes.swift`: +#### In `routes.swift`: -#### Without attachments +##### Without attachments ```swift import Mailgun @@ -118,7 +118,7 @@ func routes(_ app: Application) throws { } ``` -#### With attachments +##### With attachments ```swift import Mailgun @@ -146,7 +146,7 @@ func routes(_ app: Application) throws { } ``` -#### With template (attachments can be used in same way) +##### With template (attachments can be used in same way) ```swift import Mailgun @@ -165,7 +165,7 @@ func routes(_ app: Application) throws { } ``` -#### Setup content through Leaf +##### Setup content through Leaf Using Vapor Leaf, you can easily setup your HTML Content. @@ -203,7 +203,7 @@ func routes(_ app: Application) throws { } ``` -#### Setup routes +##### Setup routes ```swift public func configure(_ app: Application) throws { @@ -215,7 +215,7 @@ public func configure(_ app: Application) throws { } ``` -#### Handle routes +##### Handle routes ```swift import Mailgun @@ -234,7 +234,7 @@ func routes(_ app: Application) throws { } ``` -#### Creating templates +##### Creating templates ```swift import Mailgun From cd36da0cd21d037d03061f2132db561db566e68b Mon Sep 17 00:00:00 2001 From: iMike Date: Sun, 19 Jan 2020 16:38:46 +0400 Subject: [PATCH 13/15] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 110ed23..809ab91 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Vapor Mailgun Service -[![Slack](https://img.shields.io/badge/join-slack-745EAF.svg?style=flat)](https://vapor.team) -[![Platforms](https://img.shields.io/badge/platforms-macOS%2010.13%20|%20Ubuntu%2016.04%20LTS-ff0000.svg?style=flat)](http://cocoapods.org/pods/FASwift) +[![Discord](https://img.shields.io/badge/join-discord-745EAF.svg?style=flat)](https://vapor.team) +[![Platforms](https://img.shields.io/badge/platforms-macOS%2010.14%20|%20Ubuntu%2016.04%20LTS-ff0000.svg?style=flat)](http://cocoapods.org/pods/FASwift) [![Swift 5.1](https://img.shields.io/badge/swift-5.1-orange.svg?style=flat)](http://swift.org) [![Vapor 4](https://img.shields.io/badge/vapor-4.0-blue.svg?style=flat)](https://vapor.codes) From e2d8bedcbd4da38a7735a21b2264fafd1f05eea9 Mon Sep 17 00:00:00 2001 From: Mihael Isaev Date: Mon, 20 Jan 2020 03:13:03 +0400 Subject: [PATCH 14/15] Change `Storage` to `struct` --- Sources/Mailgun/Storage.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Mailgun/Storage.swift b/Sources/Mailgun/Storage.swift index 178417e..6471449 100644 --- a/Sources/Mailgun/Storage.swift +++ b/Sources/Mailgun/Storage.swift @@ -1,6 +1,6 @@ import Vapor -public class MailgunStorage { +public struct MailgunStorage { let application: Application init (_ application: Application) { @@ -16,7 +16,7 @@ public class MailgunStorage { get { application.storage[ConfigurationKey.self] } - set { + nonmutating set { application.storage[ConfigurationKey.self] = newValue } } @@ -30,7 +30,7 @@ public class MailgunStorage { get { application.storage[DefaultDomainKey.self] } - set { + nonmutating set { application.storage[DefaultDomainKey.self] = newValue } } From d57d4224155c289630e2860339be2fbf52c1ab54 Mon Sep 17 00:00:00 2001 From: Mihael Isaev Date: Mon, 20 Jan 2020 04:15:49 +0400 Subject: [PATCH 15/15] Improve Application and Request extensions --- Sources/Mailgun/Extensions/Mailgun+Application.swift | 6 ++++++ Sources/Mailgun/Extensions/Mailgun+Request.swift | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/Mailgun/Extensions/Mailgun+Application.swift b/Sources/Mailgun/Extensions/Mailgun+Application.swift index 9952ebd..8af55ff 100644 --- a/Sources/Mailgun/Extensions/Mailgun+Application.swift +++ b/Sources/Mailgun/Extensions/Mailgun+Application.swift @@ -1,6 +1,12 @@ import Vapor extension Application { + /// Mailgun storage, use it to configure mailgun. + /// + /// ```swift + /// app.mailgun.configuration = .environment + /// app.mailgun.defaultDomain = .init("mg.example.com", .eu) + /// ``` public var mailgun: MailgunStorage { .init(self) } diff --git a/Sources/Mailgun/Extensions/Mailgun+Request.swift b/Sources/Mailgun/Extensions/Mailgun+Request.swift index e4c9aee..d43acb7 100644 --- a/Sources/Mailgun/Extensions/Mailgun+Request.swift +++ b/Sources/Mailgun/Extensions/Mailgun+Request.swift @@ -1,11 +1,14 @@ import Vapor extension Request { - public var mailgun: MailgunStorage { - application.mailgun + /// Mailgun with default domain. + /// Default domain should be configured in advance through `app.mailgun.defaultDomain` + public func mailgun() -> Mailgun { + application.mailgun() } - public func mailgun(_ domain: MailgunDomain? = nil) -> Mailgun { + /// Mailgun with selected domain. + public func mailgun(_ domain: MailgunDomain) -> Mailgun { application.mailgun(domain) } }