diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..2f9a3ac --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,26 @@ +name: test +on: +- pull_request +jobs: + #sendgrid_macos: + # runs-on: macos-latest + # env: + # DEVELOPER_DIR: /Applications/Xcode_11.4_beta.app/Contents/Developer + # steps: + # - uses: actions/checkout@v2 + # - run: brew install vapor/tap/vapor-beta + # - run: xcrun swift test --enable-test-discovery --sanitize=thread + sendgrid_xenial: + container: + image: vapor/swift:5.2-xenial + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: swift test --enable-test-discovery --sanitize=thread + sendgrid_bionic: + container: + image: vapor/swift:5.2-bionic + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: swift test --enable-test-discovery --sanitize=thread diff --git a/.gitignore b/.gitignore index 4d463c3..3f9b25a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /*.xcodeproj Package.pins Package.resolved +/.swiftpm diff --git a/Package.swift b/Package.swift index cd47045..72b0f4c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,16 +1,23 @@ -// swift-tools-version:4.0 +// swift-tools-version:5.2 import PackageDescription let package = Package( name: "SendGrid", + platforms: [ + .macOS(.v10_15) + ], products: [ .library(name: "SendGrid", targets: ["SendGrid"]) ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "3.0.0"), - ], + .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0-rc"), + .package(url: "https://github.com/vapor-community/sendgrid-kit.git", from: "1.0.0"), + ], targets: [ - .target(name: "SendGrid", dependencies: ["Vapor"]), - .testTarget(name: "SendGridTests", dependencies: ["Vapor", "SendGrid"]) + .target(name: "SendGrid", dependencies: [ + .product(name: "Vapor", package: "vapor"), + .product(name: "SendGridKit", package: "sendgrid-kit"), + ]), + .testTarget(name: "SendGridTests", dependencies: ["SendGrid"]) ] ) diff --git a/README.md b/README.md index 465653b..6e99722 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,13 @@ # SendGrid Provider for Vapor -![Swift](http://img.shields.io/badge/swift-4.1-brightgreen.svg) -![Vapor](http://img.shields.io/badge/vapor-3.0-brightgreen.svg) -[![CircleCI](https://circleci.com/gh/vapor-community/sendgrid-provider.svg?style=shield)](https://circleci.com/gh/vapor-community/sendgrid-provider) +
Adds a mail backend for SendGrid to the Vapor web framework. Send simple emails, or leverage the full capabilities of SendGrid's V3 API. @@ -11,20 +16,24 @@ or leverage the full capabilities of SendGrid's V3 API. Add the dependency to Package.swift: ~~~~swift -.package(url: "https://github.com/vapor-community/sendgrid-provider.git", from: "3.0.0") +.package(url: "https://github.com/vapor-community/sendgrid.git", from: "4.0.0") ~~~~ -Register the config and the provider. -~~~~swift -let config = SendGridConfig(apiKey: "SG.something") - -services.register(config) +Make sure `SENDGRID_API_KEY` is set in your environment. This can be set in the +Xcode scheme, or specified in your `docker-compose.yml`, or even provided as +part of a `swift run` command. -try services.register(SendGridProvider()) +Optionally, explicitly initialize the provider (this is strongly recommended, as +otherwise a missing API key will cause a fatal error some time later in your +application): -app = try Application(services: services) +~~~~swift +app.sendgrid.initialize() +~~~~ -sendGridClient = try app.make(SendGridClient.self) +Now you can access the client at any time: +~~~~swift +app.sendgrid.client ~~~~ ## Using the API @@ -36,20 +45,20 @@ Usage in a route closure would be as followed: import SendGrid let email = SendGridEmail(…) -let sendGridClient = try req.make(SendGridClient.self) -try sendGridClient.send([email], on: req.eventLoop) +return req.application.sendgrid.client.send([email], on: req.eventLoop) ~~~~ ## Error handling -If the request to the API failed for any reason a `SendGridError` is `thrown` and has an `errors` property that contains an array of errors returned by the API. -Simply ensure you catch errors thrown like any other throwing function +If the request to the API failed for any reason a `SendGridError` is the result +of the future, and has an `errors` property that contains an array of errors +returned by the API: ~~~~swift -do { - try sendgridClient.send(...) -} -catch let error as SendGridError { - print(error) +return req.application.sendgrid.client.send([email], on: req.eventLoop).flatMapError { error in + if let sendgridError = error as? SendGridError { + req.logger.error("\(error)") + } + // ... } ~~~~ diff --git a/Sourcery/LinuxMain.stencil b/Sourcery/LinuxMain.stencil deleted file mode 100644 index 5b3f946..0000000 --- a/Sourcery/LinuxMain.stencil +++ /dev/null @@ -1,17 +0,0 @@ -// sourcery:file:Tests/LinuxMain.swift -import XCTest -{{ argument.testimports }} - -{% for type in types.classes|based:"XCTestCase" %} -{% if not type.annotations.disableTests %}extension {{ type.name }} { -static var allTests = [ -{% for method in type.methods %}{% if method.parameters.count == 0 and method.shortName|hasPrefix:"test" %} ("{{ method.shortName }}", {{ method.shortName }}), -{% endif %}{% endfor %}] -} - -{% endif %}{% endfor %} - -XCTMain([ -{% for type in types.classes|based:"XCTestCase" %}{% if not type.annotations.disableTests %} testCase({{ type.name }}.allTests), -{% endif %}{% endfor %}]) -// sourcery:end diff --git a/Sources/SendGrid/Application+SendGrid.swift b/Sources/SendGrid/Application+SendGrid.swift new file mode 100644 index 0000000..117906d --- /dev/null +++ b/Sources/SendGrid/Application+SendGrid.swift @@ -0,0 +1,40 @@ +import Vapor +import SendGridKit + +extension Application { + public struct Sendgrid { + private final class Storage { + let apiKey: String + + init(apiKey: String) { + self.apiKey = apiKey + } + } + + private struct Key: StorageKey { + typealias Value = Storage + } + + private var storage: Storage { + if self.application.storage[Key.self] == nil { + self.initialize() + } + return self.application.storage[Key.self]! + } + + public func initialize() { + guard let apiKey = Environment.process.SENDGRID_API_KEY else { + fatalError("No sendgrid API key provided") + } + + self.application.storage[Key.self] = .init(apiKey: apiKey) + } + + fileprivate let application: Application + + public var client: SendGridClient { .init(httpClient: self.application.client.http, apiKey: self.storage.apiKey) } + } + + public var sendgrid: Sendgrid { .init(application: self) } +} + diff --git a/Sources/SendGrid/Exports.swift b/Sources/SendGrid/Exports.swift new file mode 100644 index 0000000..4f8738f --- /dev/null +++ b/Sources/SendGrid/Exports.swift @@ -0,0 +1 @@ +@_exported import SendGridKit diff --git a/Sources/SendGrid/Models/AdvancedSuppressionManager.swift b/Sources/SendGrid/Models/AdvancedSuppressionManager.swift deleted file mode 100644 index ade8efb..0000000 --- a/Sources/SendGrid/Models/AdvancedSuppressionManager.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Vapor - -public struct AdvancedSuppressionManager: Content { - /// The unsubscribe group to associate with this email. - public var groupId: Int? - - /// An array containing the unsubscribe groups that you would like to be displayed on the unsubscribe preferences page. - public var groupsToDisplay: [String]? - - public init(groupId: Int? = nil, - groupsToDisplay: [String]? = nil) { - self.groupId = groupId - self.groupsToDisplay = groupsToDisplay - } - - public enum CodingKeys: String, CodingKey { - case groupId = "group_id" - case groupsToDisplay = "groups_to_display" - } -} diff --git a/Sources/SendGrid/Models/EmailAddress.swift b/Sources/SendGrid/Models/EmailAddress.swift deleted file mode 100644 index d7a4ac9..0000000 --- a/Sources/SendGrid/Models/EmailAddress.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// EmailAddress.swift -// Sendgrid -// -// Created by Andrew Edwards on 3/29/18. -// - -import Vapor - -public struct EmailAddress: Content { - /// format: email - public var email: String? - - /// The name of the person to whom you are sending an email. - public var name: String? - - public init(email: String? = nil, - name: String? = nil) { - self.email = email - self.name = name - } -} diff --git a/Sources/SendGrid/Models/EmailAttachment.swift b/Sources/SendGrid/Models/EmailAttachment.swift deleted file mode 100644 index 0c54332..0000000 --- a/Sources/SendGrid/Models/EmailAttachment.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// EmailAttachment.swift -// Sendgrid -// -// Created by Andrew Edwards on 3/29/18. -// - -import Vapor - -public struct EmailAttachment: Content { - - /// The Base64 encoded content of the attachment. - public var content: String? - - /// The mime type of the content you are attaching. For example, “text/plain” or “text/html”. - public var type: String? - - /// The filename of the attachment. - public var filename: String? - - /// The content-disposition of the attachment specifying how you would like the attachment to be displayed. - public var disposition: String? - - /// The content id for the attachment. This is used when the disposition is set to “inline” and the attachment is an image, allowing the file to be displayed within the body of your email. - public var contentId: String? - - public init(content: String? = nil, - type: String? = nil, - filename: String? = nil, - disposition: String? = nil, - contentId: String? = nil) { - self.content = content - self.type = type - self.filename = filename - self.disposition = disposition - self.contentId = contentId - } - - public enum CodingKeys: String, CodingKey { - case content - case type - case filename - case disposition - case contentId = "content_id" - } -} diff --git a/Sources/SendGrid/Models/MailSettings.swift b/Sources/SendGrid/Models/MailSettings.swift deleted file mode 100644 index 735e143..0000000 --- a/Sources/SendGrid/Models/MailSettings.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// MailSettings.swift -// Sendgrid -// -// Created by Andrew Edwards on 3/29/18. -// - -import Vapor - -public struct MailSettings: Content { - /// This allows you to have a blind carbon copy automatically sent to the specified email address for every email that is sent. - public var bcc: BCC? - - /// Allows you to bypass all unsubscribe groups and suppressions to ensure that the email is delivered to every single recipient. This should only be used in emergencies when it is absolutely necessary that every recipient receives your email. - public var bypassListManagement: BypassListManagement? - - /// The default footer that you would like included on every email. - public var footer: Footer? - - /// This allows you to send a test email to ensure that your request body is valid and formatted correctly. - public var sandboxMode: SandboxMode? - - /// This allows you to test the content of your email for spam. - public var spamCheck: SpamCheck? - - public init(bcc: BCC? = nil, - bypassListManagement: BypassListManagement? = nil, - footer: Footer? = nil, - sandboxMode: SandboxMode? = nil, - spamCheck: SpamCheck? = nil) { - self.bcc = bcc - self.bypassListManagement = bypassListManagement - self.footer = footer - self.sandboxMode = sandboxMode - self.spamCheck = spamCheck - } - - public enum CodingKeys: String, CodingKey { - case bcc - case bypassListManagement = "bypass_list_management" - case footer - case sandboxMode = "sandbox_mode" - case spamCheck = "spam_check" - } -} - -public struct BCC: Content { - /// Indicates if this setting is enabled. - public var enable: Bool? - public var email: String? - - public init(enable: Bool? = nil, - email: String? = nil) { - self.enable = enable - self.email = email - } -} - -public struct BypassListManagement: Content { - /// Indicates if this setting is enabled. - public var enable: Bool? - - public init(enable: Bool? = nil) { - self.enable = enable - } -} - -public struct Footer: Content { - /// Indicates if this setting is enabled. - public var enable: Bool? - - /// The plain text content of your footer. - public var text: String? - - /// The HTML content of your footer. - public var html: String? - - public init(enable: Bool? = nil, - text: String? = nil, - html: String? = nil) { - self.enable = enable - self.text = text - self.html = html - } -} - -public struct SandboxMode: Content { - /// Indicates if this setting is enabled. - public var enable: Bool? - - public init(enable: Bool? = nil) { - self.enable = enable - } -} - -public struct SpamCheck: Content { - /// Indicates if this setting is enabled. - public var enable: Bool? - - /// The threshold used to determine if your content qualifies as spam on a scale from 1 to 10, with 10 being most strict, or most likely to be considered as spam. - public var threshold: Int? - - /// An Inbound Parse URL that you would like a copy of your email along with the spam report to be sent to. - public var postToUrl: String? - - public init(enable: Bool? = nil, - threshold: Int? = nil, - postToUrl: String? = nil) { - self.enable = enable - self.threshold = threshold - self.postToUrl = postToUrl - } - - public enum CodingKeys: String, CodingKey { - case enable - case threshold - case postToUrl = "post_to_url" - } -} diff --git a/Sources/SendGrid/Models/Personalization.swift b/Sources/SendGrid/Models/Personalization.swift deleted file mode 100644 index 6203f27..0000000 --- a/Sources/SendGrid/Models/Personalization.swift +++ /dev/null @@ -1,63 +0,0 @@ -import Vapor - -public struct Personalization: Content { - - /// An array of recipients. Each object within this array may contain the name, but must always contain the email, of a recipient. - public var to: [EmailAddress]? - - /// An array of recipients who will receive a copy of your email. Each object within this array may contain the name, but must always contain the email, of a recipient. - public var cc: [EmailAddress]? - - /// An array of recipients who will receive a blind carbon copy of your email. Each object within this array may contain the name, but must always contain the email, of a recipient. - public var bcc: [EmailAddress]? - - /// The subject of your email. - public var subject: String? - - /// A collection of JSON key/value pairs allowing you to specify specific handling instructions for your email. - public var headers: [String: String]? - - /// A collection of key/value pairs following the pattern "substitution_tag":"value to substitute". - public var substitutions: [String: String]? - - /// A collection of key/value pairs following the pattern "key":"value" to substitute handlebar template data - public var dynamicTemplateData: [String: String]? - - /// Values that are specific to this personalization that will be carried along with the email and its activity data. - public var customArgs: [String: String]? - - /// A unix timestamp allowing you to specify when you want your email to be delivered. Scheduling more than 72 hours in advance is forbidden. - public var sendAt: Date? - - public init(to: [EmailAddress]? = nil, - cc: [EmailAddress]? = nil, - bcc: [EmailAddress]? = nil, - subject: String? = nil, - headers: [String: String]? = nil, - substitutions: [String: String]? = nil, - dynamicTemplateData: [String: String]? = nil, - customArgs: [String: String]? = nil, - sendAt: Date? = nil) { - self.to = to - self.cc = cc - self.bcc = bcc - self.subject = subject - self.headers = headers - self.substitutions = substitutions - self.dynamicTemplateData = dynamicTemplateData - self.customArgs = customArgs - self.sendAt = sendAt - } - - public enum CodingKeys: String, CodingKey { - case to - case cc - case bcc - case subject - case headers - case substitutions - case customArgs = "custom_args" - case dynamicTemplateData = "dynamic_template_data" - case sendAt = "send_at" - } -} diff --git a/Sources/SendGrid/Models/SendGridClient.swift b/Sources/SendGrid/Models/SendGridClient.swift deleted file mode 100644 index 3971a6f..0000000 --- a/Sources/SendGrid/Models/SendGridClient.swift +++ /dev/null @@ -1,41 +0,0 @@ -import Vapor - -public final class SendGridClient: Service { - let httpClient: Client - let apiKey: String - let apiEndpoint = "https://api.sendgrid.com/v3/mail/send" - - public init(client: Client, apiKey: String) { - self.httpClient = client - self.apiKey = apiKey - } - - public func send(_ emails: [SendGridEmail], on worker: Worker) throws -> Future