diff --git a/Package.swift b/Package.swift index f614928..6326f66 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,4 @@ -// swift-tools-version:5.2 -// The swift-tools-version declares the minimum version of Swift required to build this package. - +// swift-tools-version:5.3 import PackageDescription let package = Package( @@ -9,26 +7,31 @@ let package = Package( .macOS(.v10_15) ], products: [ - // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "Mailgun", - targets: ["Mailgun"]), + targets: ["Mailgun"] + ), ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0") + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), + .package(url: "https://github.com/vapor-community/mailgun-kit.git", .branch("v1")), + .package(url: "https://github.com/vapor/email.git", .branch("api")), ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "Mailgun", dependencies: [ .product(name: "Vapor", package: "vapor"), - ]), + .product(name: "Email", package: "email"), + .product(name: "MailgunKit", package: "mailgun-kit"), + ] + ), .testTarget( name: "MailgunTests", dependencies: [ .target(name: "Mailgun"), - ]), + .product(name: "XCTVapor", package: "vapor"), + ] + ), ] ) diff --git a/Sources/Mailgun/Emails+Mailgun.swift b/Sources/Mailgun/Emails+Mailgun.swift new file mode 100644 index 0000000..6ac852d --- /dev/null +++ b/Sources/Mailgun/Emails+Mailgun.swift @@ -0,0 +1,18 @@ +import Vapor +import Email +import MailgunKit + +extension Application.Emails.Provider { + public static func mailgun(_ configuration: Mailgun.Configuration) -> Self { + .init { app in + app.emails.use { + LiveMailgunClient( + config: configuration, + eventLoop: $0.eventLoopGroup.next(), + httpClient: $0.http.client.shared, + logger: $0.logger + ) + } + } + } +} diff --git a/Sources/Mailgun/Enums/Error.swift b/Sources/Mailgun/Enums/Error.swift deleted file mode 100644 index 6d2bd5c..0000000 --- a/Sources/Mailgun/Enums/Error.swift +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index f7ad926..0000000 --- a/Sources/Mailgun/Enums/Region.swift +++ /dev/null @@ -1,5 +0,0 @@ -/// Describes a region: US or EU -public enum MailgunRegion: String { - case us - case eu -} diff --git a/Sources/Mailgun/Exports.swift b/Sources/Mailgun/Exports.swift new file mode 100644 index 0000000..c54f28c --- /dev/null +++ b/Sources/Mailgun/Exports.swift @@ -0,0 +1,2 @@ +@_exported import MailgunKit +@_exported import Email diff --git a/Sources/Mailgun/Extensions/Mailgun+Application.swift b/Sources/Mailgun/Extensions/Mailgun+Application.swift deleted file mode 100644 index bb53ba3..0000000 --- a/Sources/Mailgun/Extensions/Mailgun+Application.swift +++ /dev/null @@ -1,105 +0,0 @@ -import Vapor - -extension Application { - public struct Mailgun { - public typealias MailgunFactory = (Application, MailgunDomain?) -> MailgunProvider - - public struct Provider { - public static var live: Self { - .init { - $0.mailgun.use { app, domain in - guard let config = app.mailgun.configuration else { - fatalError("Mailgun not configured, use: app.mailgun.configuration = .init()") - } - - let useDomain: MailgunDomain - - if let domain = domain { - useDomain = domain - } else { - guard let defaultDomain = app.mailgun.defaultDomain else { - fatalError("Mailgun default domain not configured, use: app.mailgun.defaultDomain = .init()") - } - - useDomain = defaultDomain - } - - return MailgunClient( - config: config, - eventLoop: app.eventLoopGroup.next(), - client: app.client, - domain: useDomain - ) - } - } - } - - public let run: ((Application) -> Void) - - public init(_ run: @escaping ((Application) -> Void)) { - self.run = run - } - } - - let app: Application - - private final class Storage { - var defaultDomain: MailgunDomain? - var configuration: MailgunConfiguration? - var makeClient: MailgunFactory? - - init() {} - } - - private struct Key: StorageKey { - typealias Value = Storage - } - - private var storage: Storage { - if app.storage[Key.self] == nil { - self.initialize() - } - - return app.storage[Key.self]! - } - - public func use(_ make: @escaping MailgunFactory) { - storage.makeClient = make - } - - public func use(_ provider: Application.Mailgun.Provider) { - provider.run(app) - } - - private func initialize() { - app.storage[Key.self] = .init() - app.mailgun.use(.live) - } - - public var configuration: MailgunConfiguration? { - get { storage.configuration } - nonmutating set { storage.configuration = newValue } - } - - public var defaultDomain: MailgunDomain? { - get { storage.defaultDomain } - nonmutating set { storage.defaultDomain = newValue } - } - - public func client(_ domain: MailgunDomain? = nil) -> MailgunProvider { - guard let makeClient = storage.makeClient else { - fatalError("Mailgun not configured, use: app.mailgun.use(.real)") - } - - return makeClient(app, domain) - } - } - - public var mailgun: Mailgun { - .init(app: self) - } - - public func mailgun(_ domain: MailgunDomain? = nil) -> MailgunProvider { - self.mailgun.client(domain) - } -} diff --git a/Sources/Mailgun/Extensions/Mailgun+Encode.swift b/Sources/Mailgun/Extensions/Mailgun+Encode.swift deleted file mode 100644 index d3dffbe..0000000 --- a/Sources/Mailgun/Extensions/Mailgun+Encode.swift +++ /dev/null @@ -1,12 +0,0 @@ -extension MailgunClient { - 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 deleted file mode 100644 index 4028efe..0000000 --- a/Sources/Mailgun/Extensions/Mailgun+ParseResponse.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Vapor - -extension MailgunClient { - 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 deleted file mode 100644 index e5cf0e2..0000000 --- a/Sources/Mailgun/Extensions/Mailgun+PostRequest.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Vapor -import AsyncHTTPClient - -extension MailgunClient { - func postRequest(_ content: Message, endpoint: String) -> EventLoopFuture { - do { - let authKeyEncoded = try self.encode(apiKey: config.apiKey) - var headers = HTTPHeaders() - headers.add(name: .authorization, value: "Basic \(authKeyEncoded)") - - let mailgunURI = URI(string: "\(self.baseApiUrl)/\(self.domain.domain)/\(endpoint)") - - return self.client.post(mailgunURI, headers: headers, beforeSend: { req in - try req.content.encode(content) - }).flatMapThrowing { - try self.parse(response: $0) - } - } catch { - return eventLoop.makeFailedFuture(error) - } - } -} diff --git a/Sources/Mailgun/Extensions/Mailgun+Request.swift b/Sources/Mailgun/Extensions/Mailgun+Request.swift deleted file mode 100644 index 3582ce8..0000000 --- a/Sources/Mailgun/Extensions/Mailgun+Request.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Vapor - -extension Request { - /// Mailgun with default domain. - /// Default domain should be configured in advance through `app.mailgun.defaultDomain` - public func mailgun() -> MailgunProvider { - application.mailgun().delegating(to: self.eventLoop) - } - - /// Mailgun with selected domain. - public func mailgun(_ domain: MailgunDomain) -> MailgunProvider { - application.mailgun(domain).delegating(to: self.eventLoop) - } -} diff --git a/Sources/Mailgun/LiveMailgunClient+EmailClient.swift b/Sources/Mailgun/LiveMailgunClient+EmailClient.swift new file mode 100644 index 0000000..bc3b79c --- /dev/null +++ b/Sources/Mailgun/LiveMailgunClient+EmailClient.swift @@ -0,0 +1,87 @@ +import Vapor +import MailgunKit +import Email +import NIO + +public enum VaporMailgunError: Error { + case failedToSendAttachment(Vapor.File) +} + +extension LiveMailgunClient: EmailClient { + public func send(_ messages: [EmailMessage]) -> EventLoopFuture { + do { + return try messages.map { message -> Mailgun.Message in + let html: String? + let text: String? + + switch message.content { + case let .text(_text): + text = _text + html = nil + case let .html(_html): + html = _html + text = nil + case let .universal(_text, _html): + text = _text + html = _html + } + + let attachments = try message.attachments?.toMailgunAttachments() ?? [] + let inlines = try message.attachments?.toMailgunInlineAttachments() ?? [] + + return Mailgun.Message( + from: message.from.fullAddress, + to: message.to.map(\.mailgun), + replyTo: message.replyTo?.fullAddress, + cc: message.cc?.map(\.mailgun), + bcc: message.bcc?.map(\.mailgun), + subject: message.subject, + text: text ?? "", + html: html, + attachments: attachments, + inline: inlines + ) + } + .map { self.sendRequest(.send($0)).transform(to: ()) } + .flatten(on: self.eventLoop) + } catch { + return self.eventLoop.future(error: error) + } + } +} + +extension Collection where Element == EmailAttachment { + func toMailgunAttachments() throws -> [Mailgun.File] { + try self.reduce(into: []) { result, attachment in + guard case let .attachment(file) = attachment else { return } + return try result.append(file.toMailgunFile()) + } + } + + func toMailgunInlineAttachments() throws -> [Mailgun.File] { + try self.reduce(into: []) { result, attachment in + guard case let .inline(file) = attachment else { return } + return try result.append(file.toMailgunFile()) + } + } +} + +extension Vapor.File { + func toMailgunFile() throws -> Mailgun.File { + guard let contentType = self.contentType?.serialize() else { + throw VaporMailgunError.failedToSendAttachment(self) + } + + return .init( + data: self.data, + filename: self.filename, + contentType: contentType + ) + } +} + +extension EmailAddress { + var mailgun: Mailgun.FullEmail { + (self.email, self.name) + } +} diff --git a/Sources/Mailgun/Mailgun.swift b/Sources/Mailgun/Mailgun.swift deleted file mode 100644 index 37ef392..0000000 --- a/Sources/Mailgun/Mailgun.swift +++ /dev/null @@ -1,101 +0,0 @@ -import Vapor -import Foundation - -// MARK: - Service -public protocol MailgunProvider { - func send(_ content: MailgunMessage) -> EventLoopFuture - func send(_ content: MailgunTemplateMessage) -> EventLoopFuture - func setup(forwarding: MailgunRouteSetup) -> EventLoopFuture - func createTemplate(_ template: MailgunTemplate) -> EventLoopFuture - - func delegating(to eventLoop: EventLoop) -> MailgunProvider -} - -public struct MailgunClient: MailgunProvider { - let eventLoop: EventLoop - let config: MailgunConfiguration - let domain: MailgunDomain - let client: Client - - // MARK: Initialization - public init( - config: MailgunConfiguration, - eventLoop: EventLoop, - client: Client, - domain: MailgunDomain - ) { - self.config = config - self.eventLoop = eventLoop - self.client = client - self.domain = domain - } - - public func delegating(to eventLoop: EventLoop) -> MailgunProvider { - MailgunClient(config: config, eventLoop: eventLoop, client: client.delegating(to: eventLoop), domain: domain) - } -} - -// MARK: - Send message - -extension MailgunClient { - /// 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: - /// - content: Message - /// - container: Container - /// - Returns: Future - public func send(_ content: MailgunMessage) -> EventLoopFuture { - postRequest(content, endpoint: "messages") - } - - /// Send message - /// - /// - Parameters: - /// - content: TemplateMessage - /// - container: Container - /// - Returns: Future - public func send(_ content: MailgunTemplateMessage) -> EventLoopFuture { - postRequest(content, endpoint: "messages") - } - - /// Setup forwarding - /// - /// - Parameters: - /// - setup: RouteSetup - /// - container: Container - /// - Returns: Future - public func setup(forwarding setup: MailgunRouteSetup) -> EventLoopFuture { - postRequest(setup, endpoint: "v3/routes") - } - - /// Create template - /// - /// - Parameters: - /// - template: Template - /// - container: Container - /// - Returns: Future - public func createTemplate(_ template: MailgunTemplate) -> EventLoopFuture { - postRequest(template, endpoint: "templates") - } -} - -// MARK: - Conversions - -extension Array where Element == MailgunMessage.FullEmail { - var stringArray: [String] { - map { entry in - guard let name = entry.name else { - return entry.email - } - return "\"\(name) <\(entry.email)>\"" - } - } -} diff --git a/Sources/Mailgun/Models/Configuration.swift b/Sources/Mailgun/Models/Configuration.swift deleted file mode 100644 index 21d3ad8..0000000 --- a/Sources/Mailgun/Models/Configuration.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import Vapor - -public struct MailgunConfiguration { - /// API key (including "key-" prefix) - public let apiKey: String - - /// Initializer - /// - /// - Parameters: - /// - apiKey: API key including "key-" prefix - /// - domain: API domain - public init(apiKey: String) { - self.apiKey = apiKey - } - - /// It will try to initialize configuration with environment variables: - /// - MAILGUN_API_KEY - public static var environment: MailgunConfiguration { - guard let apiKey = Environment.get("MAILGUN_API_KEY") else { - fatalError("Mailgun environmant variables not set") - } - return .init(apiKey: apiKey) - } -} diff --git a/Sources/Mailgun/Models/Domain.swift b/Sources/Mailgun/Models/Domain.swift deleted file mode 100644 index 194c13a..0000000 --- a/Sources/Mailgun/Models/Domain.swift +++ /dev/null @@ -1,9 +0,0 @@ -public struct MailgunDomain { - public let domain: String - public let region: MailgunRegion - - public init(_ domain: String, _ region: MailgunRegion) { - self.domain = domain - self.region = region - } -} diff --git a/Sources/Mailgun/Models/ErrorResponse.swift b/Sources/Mailgun/Models/ErrorResponse.swift deleted file mode 100644 index cb8e261..0000000 --- a/Sources/Mailgun/Models/ErrorResponse.swift +++ /dev/null @@ -1,5 +0,0 @@ -/// 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 deleted file mode 100644 index 2313c49..0000000 --- a/Sources/Mailgun/Models/IncomingMessage.swift +++ /dev/null @@ -1,89 +0,0 @@ -import Vapor - -public struct MailgunIncomingMessage: Content { - public static var defaultContentType: HTTPMediaType = .formData - - public let recipient: String - public let sender: String - public let from: String - public let subject: String - public let bodyPlain: String - public let strippedText: String - public let strippedSignature: String? - public let bodyHTML: String - public let strippedHTML: String - public let messageHeaders: String - public let contentIdMap: String - public let attachments: [File] - - enum CodingKeys: String, CodingKey { - case recipient - case sender - case from - case subject - case bodyPlain = "body-plain" - case strippedText = "stripped-text" - case strippedSignature = "stripped-signature" - case bodyHTML = "body-html" - case strippedHTML = "stripped-html" - case messageHeaders = "message-headers" - case contentIdMap = "content-id-map" - case attachments - } - - struct DynamicAttachmentKey: CodingKey { - var stringValue: String - - init?(stringValue: String) { - guard stringValue.hasPrefix("attachment-") else { return nil } - guard let lastKey = stringValue.components(separatedBy: "-").last, - let _ = Int(lastKey) - else { return nil} - self.stringValue = stringValue - } - - var intValue: Int? - - init?(intValue: Int) { - return nil - } - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - recipient = try container.decode(String.self, forKey: .recipient) - sender = try container.decode(String.self, forKey: .sender) - from = try container.decode(String.self, forKey: .from) - subject = try container.decode(String.self, forKey: .subject) - bodyPlain = try container.decode(String.self, forKey: .bodyPlain) - strippedText = try container.decode(String.self, forKey: .strippedText) - strippedSignature = try container.decodeIfPresent(String.self, forKey: .strippedSignature) - bodyHTML = try container.decode(String.self, forKey: .bodyHTML) - strippedHTML = try container.decode(String.self, forKey: .strippedHTML) - messageHeaders = try container.decode(String.self, forKey: .messageHeaders) - contentIdMap = try container.decode(String.self, forKey: .contentIdMap) - - var _attachments: [File] = [] - let attachmentsContainer = try decoder.container(keyedBy: DynamicAttachmentKey.self) - try attachmentsContainer.allKeys.forEach { attachmentKey in - _attachments.append(try attachmentsContainer.decode(File.self, forKey: attachmentKey)) - } - attachments = _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 deleted file mode 100644 index 0e4085e..0000000 --- a/Sources/Mailgun/Models/Message.swift +++ /dev/null @@ -1,71 +0,0 @@ -import Vapor - -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 deleted file mode 100644 index cc56617..0000000 --- a/Sources/Mailgun/Models/RouteSetup.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Vapor -import Foundation - -public struct MailgunRouteSetup: Content { - public static var defaultContentType: HTTPMediaType = .urlEncodedForm - - public let priority: Int - public let description: String - public let filter: String - public let action: [String] - - public init(forwardURL: String, description: String) { - self.priority = 0 - self.description = description - self.filter = "catch_all()" - self.action = ["forward('\(forwardURL)')", "stop()"] - } - - enum CodingKeys: String, CodingKey { - case priority - case description - case filter = "expression" - case action - } -} diff --git a/Sources/Mailgun/Models/Template.swift b/Sources/Mailgun/Models/Template.swift deleted file mode 100644 index 5e9c1c3..0000000 --- a/Sources/Mailgun/Models/Template.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Vapor - -/// 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 - } -} - diff --git a/Sources/Mailgun/Models/TemplateMessage.swift b/Sources/Mailgun/Models/TemplateMessage.swift deleted file mode 100644 index 16befdb..0000000 --- a/Sources/Mailgun/Models/TemplateMessage.swift +++ /dev/null @@ -1,101 +0,0 @@ -import Vapor - -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 - } -} diff --git a/Tests/MailgunTests/MailgunTests.swift b/Tests/MailgunTests/MailgunTests.swift index a04980d..8bd2d1d 100644 --- a/Tests/MailgunTests/MailgunTests.swift +++ b/Tests/MailgunTests/MailgunTests.swift @@ -1,16 +1,28 @@ import XCTest +import XCTVapor +import Email @testable import Mailgun final class MailgunTests: XCTestCase { - func testExample() { - // 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!") + func test_sendEmail() throws { + let app = Application(.testing) + defer { app.shutdown() } + + let domain = Mailgun.Domain("", .eu) + let config = Mailgun.Configuration(apiKey: "", defaultDomain: domain) + app.emails.use(.mailgun(config)) + + // Multiple attachments will result in a warning. + let message = EmailMessage.init( + from: "test@test.com", + to: "mads@test.com", + subject: "test email", + content: .html("

Hey mads!

"), + attachments: [ + .attachment(File.init(data: "this is a text file", filename: "test.txt")), + .attachment(File.init(data: "this is a text file2", filename: "test2.txt")) + ] + ) + try app.email.send(message).wait() } - - - static var allTests = [ - ("testExample", testExample), - ] }