From 3bf881ff3c1d0070764293e5b4962b4ad65aa767 Mon Sep 17 00:00:00 2001 From: Shine Labs Date: Sun, 15 Mar 2020 00:11:37 -0600 Subject: [PATCH 1/4] Update to use async-http-client --- .gitignore | 7 + CHANGELOG.md | 0 LICENSE | 22 +++ Package.swift | 23 +++ README.md | 52 +++++ .../Models/AdvancedSuppressionManager.swift | 27 +++ Sources/SendGridKit/Models/EmailAddress.swift | 15 ++ .../SendGridKit/Models/EmailAttachment.swift | 55 ++++++ Sources/SendGridKit/Models/MailSettings.swift | 129 +++++++++++++ .../SendGridKit/Models/Personalization.swift | 76 ++++++++ .../SendGridKit/Models/SendGridEmail.swift | 130 +++++++++++++ .../SendGridKit/Models/SendGridError.swift | 11 ++ .../SendGridKit/Models/TrackingSettings.swift | 180 ++++++++++++++++++ Sources/SendGridKit/SendGridClient.swift | 78 ++++++++ Tests/LinuxMain.swift | 16 ++ Tests/SendGridKitTests/SendGridTestsKit.swift | 16 ++ 16 files changed, 837 insertions(+) create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/SendGridKit/Models/AdvancedSuppressionManager.swift create mode 100644 Sources/SendGridKit/Models/EmailAddress.swift create mode 100644 Sources/SendGridKit/Models/EmailAttachment.swift create mode 100644 Sources/SendGridKit/Models/MailSettings.swift create mode 100644 Sources/SendGridKit/Models/Personalization.swift create mode 100644 Sources/SendGridKit/Models/SendGridEmail.swift create mode 100644 Sources/SendGridKit/Models/SendGridError.swift create mode 100644 Sources/SendGridKit/Models/TrackingSettings.swift create mode 100644 Sources/SendGridKit/SendGridClient.swift create mode 100644 Tests/LinuxMain.swift create mode 100644 Tests/SendGridKitTests/SendGridTestsKit.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e12670c --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.build +/Packages +*.xcodeproj +Package.pins +Package.resolved +.swiftpm/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d5fc025 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ + +MIT LICENSE + +Copyright (c) 2020 Dylan Shine + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..f0a8120 --- /dev/null +++ b/Package.swift @@ -0,0 +1,23 @@ +// swift-tools-version:5.2 +import PackageDescription + +let package = Package( + name: "SendGridKit", + platforms: [ + .macOS(.v10_15), + ], + products: [ + .library(name: "SendGridKit", targets: ["SendGridKit"]), + ], + dependencies: [ + .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.0.0"), + ], + targets: [ + .target(name: "SendGridKit", dependencies: [ + .product(name: "AsyncHTTPClient", package: "async-http-client"), + ]), + .testTarget(name: "SendGridKitTests", dependencies: [ + .target(name: "SendGridKit"), + ]) + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..384da99 --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ +# SendGridKit + +![Swift](http://img.shields.io/badge/swift-5.2-brightgreen.svg) +![Vapor](http://img.shields.io/badge/vapor-4.0-brightgreen.svg) + +SendGridKit is a Swift package used to communicate with the SendGrid API for Server Side Swift Apps. + +## Setup +Add the dependency to Package.swift: + +~~~~swift +dependencies: [ + ... + .package(url: "https://github.com/vapor-community/sendgrit-kit.git", from: "1.0.0") +], +targets: [ + .target(name: "App", dependencies: [ + .product(name: "SendGridKit", package: "sendgrid-kit"), + ]), +~~~~ + +Register the config and the provider. + +~~~~swift +let httpClient = HTTPClient(...) +let sendgridClient = SendGridClient(httpClient: httpClient, apiKey: "YOUR_API_KEY") +~~~~ + +## Using the API + +You can use all of the available parameters here to build your `SendGridEmail` +Usage in a route closure would be as followed: + +~~~~swift +import SendGrid + +let email = SendGridEmail(...) +try sendGridClient.send(email, on: 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 + +~~~~swift +do { + try sendGridClient.send(...) +} +catch let error as SendGridError { + print(error) +} +~~~~ diff --git a/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift b/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift new file mode 100644 index 0000000..8b29f8d --- /dev/null +++ b/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift @@ -0,0 +1,27 @@ +import Foundation + +public struct AdvancedSuppressionManager: Encodable { + /// 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 + } + + private enum CodingKeys: String, CodingKey { + case groupId = "group_id" + case groupsToDisplay = "groups_to_display" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(groupId, forKey: .groupId) + try container.encode(groupsToDisplay, forKey: .groupsToDisplay) + } + +} diff --git a/Sources/SendGridKit/Models/EmailAddress.swift b/Sources/SendGridKit/Models/EmailAddress.swift new file mode 100644 index 0000000..28486b5 --- /dev/null +++ b/Sources/SendGridKit/Models/EmailAddress.swift @@ -0,0 +1,15 @@ +import Foundation + +public struct EmailAddress: Encodable { + /// 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/SendGridKit/Models/EmailAttachment.swift b/Sources/SendGridKit/Models/EmailAttachment.swift new file mode 100644 index 0000000..a6fcd58 --- /dev/null +++ b/Sources/SendGridKit/Models/EmailAttachment.swift @@ -0,0 +1,55 @@ +// +// EmailAttachment.swift +// Sendgrid +// +// Created by Andrew Edwards on 3/29/18. +// + +import Foundation + +public struct EmailAttachment: Encodable { + + /// 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 + } + + private enum CodingKeys: String, CodingKey { + case content + case type + case filename + case disposition + case contentId = "content_id" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(content, forKey: .content) + try container.encode(type, forKey: .type) + try container.encode(filename, forKey: .filename) + try container.encode(disposition, forKey: .disposition) + try container.encode(contentId, forKey: .contentId) + } +} diff --git a/Sources/SendGridKit/Models/MailSettings.swift b/Sources/SendGridKit/Models/MailSettings.swift new file mode 100644 index 0000000..1dc1a1e --- /dev/null +++ b/Sources/SendGridKit/Models/MailSettings.swift @@ -0,0 +1,129 @@ +import Foundation + +public struct MailSettings: Encodable { + /// 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 + } + + private enum CodingKeys: String, CodingKey { + case bcc + case bypassListManagement = "bypass_list_management" + case footer + case sandboxMode = "sandbox_mode" + case spamCheck = "spam_check" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(bcc, forKey: .bcc) + try container.encode(bypassListManagement, forKey: .bypassListManagement) + try container.encode(footer, forKey: .footer) + try container.encode(sandboxMode, forKey: .sandboxMode) + try container.encode(spamCheck, forKey: .spamCheck) + } +} + +public struct BCC: Encodable { + /// 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: Encodable { + /// Indicates if this setting is enabled. + public var enable: Bool? + + public init(enable: Bool? = nil) { + self.enable = enable + } +} + +public struct Footer: Encodable { + /// 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: Encodable { + /// Indicates if this setting is enabled. + public var enable: Bool? + + public init(enable: Bool? = nil) { + self.enable = enable + } +} + +public struct SpamCheck: Encodable { + /// 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 + } + + private enum CodingKeys: String, CodingKey { + case enable + case threshold + case postToUrl = "post_to_url" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(enable, forKey: .enable) + try container.encode(threshold, forKey: .threshold) + try container.encode(postToUrl, forKey: .postToUrl) + } + +} diff --git a/Sources/SendGridKit/Models/Personalization.swift b/Sources/SendGridKit/Models/Personalization.swift new file mode 100644 index 0000000..7bad802 --- /dev/null +++ b/Sources/SendGridKit/Models/Personalization.swift @@ -0,0 +1,76 @@ +import Foundation + +public struct Personalization: Encodable { + + /// 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 + } + + private 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" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + 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(headers, forKey: .headers) + try container.encode(substitutions, forKey: .substitutions) + try container.encode(customArgs, forKey: .customArgs) + try container.encode(dynamicTemplateData, forKey: .dynamicTemplateData) + try container.encode(sendAt, forKey: .sendAt) + } +} diff --git a/Sources/SendGridKit/Models/SendGridEmail.swift b/Sources/SendGridKit/Models/SendGridEmail.swift new file mode 100644 index 0000000..52f32db --- /dev/null +++ b/Sources/SendGridKit/Models/SendGridEmail.swift @@ -0,0 +1,130 @@ +import Foundation + +public struct SendGridEmail: Encodable { + + /// An array of messages and their metadata. Each object within personalizations can be thought of as an envelope - it defines who should receive an individual message and how that message should be handled. + public var personalizations: [Personalization]? + + public var from: EmailAddress? + + public var replyTo: EmailAddress? + + /// The global, or “message level”, subject of your email. This may be overridden by personalizations[x].subject. + public var subject: String? + + /// An array in which you may specify the content of your email. + public var content: [[String: String]]? + + /// An array of objects in which you can specify any attachments you want to include. + public var attachments: [EmailAttachment]? + + /// The id of a template that you would like to use. If you use a template that contains a subject and content (either text or html), you do not need to specify those at the personalizations nor message level. + public var templateId: String? + + /// An object of key/value pairs that define block sections of code to be used as substitutions. + public var sections: [String: String]? + + /// An object containing key/value pairs of header names and the value to substitute for them. You must ensure these are properly encoded if they contain unicode characters. Must not be one of the reserved headers. + public var headers: [String: String]? + + /// An array of category names for this message. Each category name may not exceed 255 characters. + public var categories: [String]? + + /// Values that are specific to the entire send 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. This may be overridden by the personalizations[x].send_at parameter. You can't schedule more than 72 hours in advance. + public var sendAt: Date? + + /// This ID represents a batch of emails to be sent at the same time. Including a batch_id in your request allows you include this email in that batch, and also enables you to cancel or pause the delivery of that batch. + public var batchId: String? + + /// An object allowing you to specify how to handle unsubscribes. + public var asm: AdvancedSuppressionManager? + + /// The IP Pool that you would like to send this email from. + public var ipPoolName: String? + + /// A collection of different mail settings that you can use to specify how you would like this email to be handled. + public var mailSettings: MailSettings? + + /// Settings to determine how you would like to track the metrics of how your recipients interact with your email. + public var trackingSettings: TrackingSettings? + + public init(personalizations: [Personalization]? = nil, + from: EmailAddress? = nil, + replyTo: EmailAddress? = nil, + subject: String? = nil, + content: [[String: String]]? = nil, + attachments: [EmailAttachment]? = nil, + templateId: String? = nil, + sections: [String: String]? = nil, + headers: [String: String]? = nil, + categories: [String]? = nil, + customArgs: [String: String]? = nil, + sendAt: Date? = nil, + batchId: String? = nil, + asm: AdvancedSuppressionManager? = nil, + ipPoolName: String? = nil, + mailSettings: MailSettings? = nil, + trackingSettings: TrackingSettings? = nil) { + self.personalizations = personalizations + self.from = from + self.replyTo = replyTo + self.subject = subject + self.content = content + self.attachments = attachments + self.templateId = templateId + self.sections = sections + self.headers = headers + self.categories = categories + self.customArgs = customArgs + self.sendAt = sendAt + self.batchId = batchId + self.asm = asm + self.ipPoolName = ipPoolName + self.mailSettings = mailSettings + self.trackingSettings = trackingSettings + } + + private enum CodingKeys: String, CodingKey { + case personalizations + case from + case replyTo = "reply_to" + case subject + case content + case attachments + case templateId = "template_id" + case sections + case headers + case categories + case customArgs = "custom_args" + case sendAt = "send_at" + case batchId = "batch_id" + case asm + case ipPoolName = "ip_pool_name" + case mailSettings = "mail_settings" + case trackingSettings = "tracking_settings" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(personalizations, forKey: .personalizations) + try container.encode(from, forKey: .from) + try container.encode(replyTo, forKey: .replyTo) + try container.encode(subject, forKey: .subject) + try container.encode(content, forKey: .content) + try container.encode(attachments, forKey: .attachments) + try container.encode(templateId, forKey: .templateId) + try container.encode(sections, forKey: .sections) + try container.encode(headers, forKey: .headers) + try container.encode(categories, forKey: .categories) + try container.encode(customArgs, forKey: .customArgs) + try container.encode(sendAt, forKey: .sendAt) + try container.encode(batchId, forKey: .batchId) + try container.encode(asm, forKey: .asm) + try container.encode(ipPoolName, forKey: .ipPoolName) + try container.encode(mailSettings, forKey: .mailSettings) + try container.encode(trackingSettings, forKey: .trackingSettings) + } +} diff --git a/Sources/SendGridKit/Models/SendGridError.swift b/Sources/SendGridKit/Models/SendGridError.swift new file mode 100644 index 0000000..26feb67 --- /dev/null +++ b/Sources/SendGridKit/Models/SendGridError.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct SendGridError: Error, Decodable { + public var errors: [SendGridErrorResponse]? +} + +public struct SendGridErrorResponse: Decodable { + public var message: String? + public var field: String? + public var help: String? +} diff --git a/Sources/SendGridKit/Models/TrackingSettings.swift b/Sources/SendGridKit/Models/TrackingSettings.swift new file mode 100644 index 0000000..85e59b7 --- /dev/null +++ b/Sources/SendGridKit/Models/TrackingSettings.swift @@ -0,0 +1,180 @@ +import Foundation + +public struct TrackingSettings: Encodable { + /// Allows you to track whether a recipient clicked a link in your email. + public var clickTracking: ClickTracking? + + /// Allows you to track whether the email was opened or not, but including a single pixel image in the body of the content. When the pixel is loaded, we can log that the email was opened. + public var openTracking: OpenTracking? + + /// Allows you to insert a subscription management link at the bottom of the text and html bodies of your email. If you would like to specify the location of the link within your email, you may use the substitution_tag. + public var subscriptionTracking: SubscriptionTracking? + + /// Allows you to enable tracking provided by Google Analytics. + public var ganalytics: GoogleAnalytics? + + public init(clickTracking: ClickTracking? = nil, + openTracking: OpenTracking? = nil, + subscriptionTracking: SubscriptionTracking? = nil, + ganalytics: GoogleAnalytics? = nil) { + self.clickTracking = clickTracking + self.openTracking = openTracking + self.subscriptionTracking = subscriptionTracking + self.ganalytics = ganalytics + } + + private enum CodingKeys: String, CodingKey { + case clickTracking = "click_tracking" + case openTracking = "open_tracking" + case subscriptionTracking = "subscription_tracking" + case ganalytics + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(clickTracking, forKey: .clickTracking) + try container.encode(openTracking, forKey: .openTracking) + try container.encode(subscriptionTracking, forKey: .subscriptionTracking) + try container.encode(ganalytics, forKey: .ganalytics) + } + +} + +public struct ClickTracking: Encodable { + /// Indicates if this setting is enabled. + public var enable: Bool? + + /// Indicates if this setting should be included in the text/plain portion of your email. + public var enableText: Bool? + + public init(enable: Bool? = nil, + enableText: Bool? = nil) { + self.enable = enable + self.enableText = enableText + } + + private enum CodingKeys: String, CodingKey { + case enable + case enableText = "enable_text" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(enable, forKey: .enable) + try container.encode(enableText, forKey: .enableText) + } +} + +public struct OpenTracking: Encodable { + /// Indicates if this setting is enabled. + public var enable: Bool? + + /// Allows you to specify a substitution tag that you can insert in the body of your email at a location that you desire. This tag will be replaced by the open tracking pixel. + public var substitutionTag: String? + + public init(enable: Bool? = nil, + substitutionTag: String? = nil) { + self.enable = enable + self.substitutionTag = substitutionTag + } + + private enum CodingKeys: String, CodingKey { + case enable + case substitutionTag = "substitution_tag" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(enable, forKey: .enable) + try container.encode(substitutionTag, forKey: .substitutionTag) + } +} + +public struct SubscriptionTracking: Encodable { + /// Indicates if this setting is enabled. + public var enable: Bool? + + /// Text to be appended to the email, with the subscription tracking link. You may control where the link is by using the tag <% %> + public var text: String? + + /// HTML to be appended to the email, with the subscription tracking link. You may control where the link is by using the tag <% %> + public var html: String? + + /// A tag that will be replaced with the unsubscribe URL. for example: [unsubscribe_url]. If this parameter is used, it will override both the text and html parameters. The URL of the link will be placed at the substitution tag’s location, with no additional formatting. + public var substitutionTag: String? + + public init(enable: Bool? = nil, + text: String? = nil, + html: String? = nil) { + self.enable = enable + self.text = text + self.html = html + } + + private enum CodingKeys: String, CodingKey { + case enable + case text + case html + case substitutionTag = "substitution_tag" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(enable, forKey: .enable) + try container.encode(text, forKey: .text) + try container.encode(html, forKey: .html) + try container.encode(substitutionTag, forKey: .substitutionTag) + } +} + +public struct GoogleAnalytics: Encodable { + /// Indicates if this setting is enabled. + public var enable: Bool? + + /// Name of the referrer source. (e.g. Google, SomeDomain.com, or Marketing Email) + public var utmSource: String? + + /// Name of the marketing medium. (e.g. Email) + public var utmMedium: String? + + /// Used to identify any paid keywords. + public var utmTerm: String? + + /// Used to differentiate your campaign from advertisements. + public var utmContent: String? + + /// The name of the campaign. + public var utmCampaign: String? + + public init(enable: Bool? = nil, + utmSource: String? = nil, + utmMedium: String? = nil, + utmTerm: String? = nil, + utmContent: String? = nil, + utmCampaign: String? = nil) { + self.enable = enable + self.utmSource = utmSource + self.utmMedium = utmMedium + self.utmTerm = utmTerm + self.utmContent = utmContent + self.utmCampaign = utmCampaign + } + + private enum CodingKeys: String, CodingKey { + case enable + case utmSource = "utm_source" + case utmMedium = "utm_medium" + case utmTerm = "utm_term" + case utmContent = "utm_content" + case utmCampaign = "utm_campaign" + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(enable, forKey: .enable) + try container.encode(utmSource, forKey: .utmSource) + try container.encode(utmMedium, forKey: .utmMedium) + try container.encode(utmContent, forKey: .utmContent) + try container.encode(utmCampaign, forKey: .utmCampaign) + } +} diff --git a/Sources/SendGridKit/SendGridClient.swift b/Sources/SendGridKit/SendGridClient.swift new file mode 100644 index 0000000..16cf302 --- /dev/null +++ b/Sources/SendGridKit/SendGridClient.swift @@ -0,0 +1,78 @@ +import Foundation +import NIO +import AsyncHTTPClient +import NIOHTTP1 + +public struct SendGridClient { + + let apiURL = "https://api.sendgrid.com/v3/mail/send" + let httpClient: HTTPClient + let apiKey: String + + private let encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .secondsSince1970 + return encoder + }() + + private let decoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .secondsSince1970 + return decoder + }() + + public init(httpClient: HTTPClient, apiKey: String) { + self.httpClient = httpClient + self.apiKey = apiKey + } + + public func send(emails: [SendGridEmail], on eventLoop: EventLoop) throws -> EventLoopFuture { + + let futures = emails.map { email -> EventLoopFuture in + do { + return try send(email: email, on: eventLoop) + } catch { + return eventLoop.makeFailedFuture(error) + } + } + + return EventLoopFuture.andAllSucceed(futures, on: eventLoop) + } + + public func send(email: SendGridEmail, on eventLoop: EventLoop) throws -> EventLoopFuture { + + var headers = HTTPHeaders() + headers.add(name: "Authorization", value: "Bearer \(apiKey)") + headers.add(name: "Content-Type", value: "application/json") + + let bodyData = try encoder.encode(email) + + let bodyString = String(decoding: bodyData, as: UTF8.self) + + let request = try HTTPClient.Request(url: apiURL, + method: .POST, + headers: headers, + body: .string(bodyString)) + + return httpClient.execute(request: request, + eventLoop: .delegate(on: eventLoop)) + .flatMap { response in + switch response.status { + case .ok, .accepted: + return eventLoop.makeSucceededFuture(()) + default: + + // JSONDecoder will handle empty body by throwing decoding error + let byteBuffer = response.body ?? ByteBuffer(.init()) + let responseData = Data(byteBuffer.readableBytesView) + + do { + let error = try self.decoder.decode(SendGridError.self, from: responseData) + return eventLoop.makeFailedFuture(error) + } catch { + return eventLoop.makeFailedFuture(error) + } + } + } + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..30e94b0 --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,16 @@ +// Generated using Sourcery 0.7.2 — https://github.com/krzysztofzablocki/Sourcery +// DO NOT EDIT + +import XCTest +@testable import SendGridKitTests + +extension SendGridTests { +static var allTests = [ + ("testNothing", testNothing), +] +} + + +XCTMain([ + testCase(SendGridTests.allTests), +]) diff --git a/Tests/SendGridKitTests/SendGridTestsKit.swift b/Tests/SendGridKitTests/SendGridTestsKit.swift new file mode 100644 index 0000000..4dce5cc --- /dev/null +++ b/Tests/SendGridKitTests/SendGridTestsKit.swift @@ -0,0 +1,16 @@ +import XCTest +@testable import SendGridKit +// Test inbox: https://www.mailinator.com/inbox2.jsp?public_to=vapor-sendgrid + +class SendGridKitTests: XCTestCase { + + /** + Only way we can test if our request is valid is to use an actual APi key. + Maybe we'll use the testwithvapor@gmail account for these tests if it becomes + a recurring theme we need api keys to test providers. + */ + + func testNothing() { + XCTAssertTrue(true) + } +} From 3b4030b999e01f7b82e79b7d1b6a3455c25aa668 Mon Sep 17 00:00:00 2001 From: Shine Labs Date: Sun, 15 Mar 2020 00:22:41 -0600 Subject: [PATCH 2/4] Update model property types to accuractly reflect API requirements --- .../SendGridKit/Models/AdvancedSuppressionManager.swift | 4 ++-- Sources/SendGridKit/Models/EmailAddress.swift | 4 ++-- Sources/SendGridKit/Models/EmailAttachment.swift | 8 ++++---- Sources/SendGridKit/Models/SendGridEmail.swift | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift b/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift index 8b29f8d..73c3e17 100644 --- a/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift +++ b/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift @@ -2,12 +2,12 @@ import Foundation public struct AdvancedSuppressionManager: Encodable { /// The unsubscribe group to associate with this email. - public var groupId: Int? + 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, + public init(groupId: Int, groupsToDisplay: [String]? = nil) { self.groupId = groupId self.groupsToDisplay = groupsToDisplay diff --git a/Sources/SendGridKit/Models/EmailAddress.swift b/Sources/SendGridKit/Models/EmailAddress.swift index 28486b5..fb32181 100644 --- a/Sources/SendGridKit/Models/EmailAddress.swift +++ b/Sources/SendGridKit/Models/EmailAddress.swift @@ -2,12 +2,12 @@ import Foundation public struct EmailAddress: Encodable { /// format: email - public var email: String? + 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, + public init(email: String, name: String? = nil) { self.email = email self.name = name diff --git a/Sources/SendGridKit/Models/EmailAttachment.swift b/Sources/SendGridKit/Models/EmailAttachment.swift index a6fcd58..a645455 100644 --- a/Sources/SendGridKit/Models/EmailAttachment.swift +++ b/Sources/SendGridKit/Models/EmailAttachment.swift @@ -10,13 +10,13 @@ import Foundation public struct EmailAttachment: Encodable { /// The Base64 encoded content of the attachment. - public var content: String? + 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? + public var filename: String /// The content-disposition of the attachment specifying how you would like the attachment to be displayed. public var disposition: String? @@ -24,9 +24,9 @@ public struct EmailAttachment: Encodable { /// 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, + public init(content: String, type: String? = nil, - filename: String? = nil, + filename: String, disposition: String? = nil, contentId: String? = nil) { self.content = content diff --git a/Sources/SendGridKit/Models/SendGridEmail.swift b/Sources/SendGridKit/Models/SendGridEmail.swift index 52f32db..dd8be65 100644 --- a/Sources/SendGridKit/Models/SendGridEmail.swift +++ b/Sources/SendGridKit/Models/SendGridEmail.swift @@ -13,7 +13,7 @@ public struct SendGridEmail: Encodable { public var subject: String? /// An array in which you may specify the content of your email. - public var content: [[String: String]]? + public var content: [[String: String]] /// An array of objects in which you can specify any attachments you want to include. public var attachments: [EmailAttachment]? @@ -55,7 +55,7 @@ public struct SendGridEmail: Encodable { from: EmailAddress? = nil, replyTo: EmailAddress? = nil, subject: String? = nil, - content: [[String: String]]? = nil, + content: [[String: String]], attachments: [EmailAttachment]? = nil, templateId: String? = nil, sections: [String: String]? = nil, From e09a39284cb2b7ed7b6e0ce3fb159cad3dcd00cd Mon Sep 17 00:00:00 2001 From: Shine Labs Date: Sun, 15 Mar 2020 00:28:20 -0600 Subject: [PATCH 3/4] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 384da99..711d156 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ Add the dependency to Package.swift: ~~~~swift dependencies: [ ... - .package(url: "https://github.com/vapor-community/sendgrit-kit.git", from: "1.0.0") + .package(url: "https://github.com/vapor-community/SendGridKit.git", from: "1.0.0") ], targets: [ .target(name: "App", dependencies: [ - .product(name: "SendGridKit", package: "sendgrid-kit"), + .product(name: "SendGridKit", package: "SendGridKit"), ]), ~~~~ From 1ebeb75e6a06f7ea499556bbf795a5e4862a0033 Mon Sep 17 00:00:00 2001 From: Shine Labs Date: Sun, 15 Mar 2020 20:45:48 -0600 Subject: [PATCH 4/4] Update Package name a README --- Package.swift | 2 +- README.md | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index f0a8120..3f62bc7 100644 --- a/Package.swift +++ b/Package.swift @@ -2,7 +2,7 @@ import PackageDescription let package = Package( - name: "SendGridKit", + name: "sendgrid-kit", platforms: [ .macOS(.v10_15), ], diff --git a/README.md b/README.md index 711d156..e8d21c8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # SendGridKit ![Swift](http://img.shields.io/badge/swift-5.2-brightgreen.svg) -![Vapor](http://img.shields.io/badge/vapor-4.0-brightgreen.svg) SendGridKit is a Swift package used to communicate with the SendGrid API for Server Side Swift Apps. @@ -11,11 +10,11 @@ Add the dependency to Package.swift: ~~~~swift dependencies: [ ... - .package(url: "https://github.com/vapor-community/SendGridKit.git", from: "1.0.0") + .package(url: "https://github.com/vapor-community/sendgrid-kit.git", from: "1.0.0") ], targets: [ .target(name: "App", dependencies: [ - .product(name: "SendGridKit", package: "SendGridKit"), + .product(name: "SendGridKit", package: "sendgrid-kit"), ]), ~~~~