From 58045157cd80054bbb39f44b76d287291b7a755a Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Sat, 7 Sep 2024 22:23:48 +0200 Subject: [PATCH 1/3] Update to Swift 5.10 (#18) * Improve DocC * Make everything `Sendable` * Add DocC catalog * Improve test coverage --- .github/workflows/test.yml | 13 ++++ .spi.yml | 4 ++ CHANGELOG.md | 0 Package.swift | 35 +++++++--- README.md | 66 ++++++++++++------- .../Models/AdvancedSuppressionManager.swift | 3 +- Sources/SendGridKit/Models/EmailAddress.swift | 4 +- .../SendGridKit/Models/EmailAttachment.swift | 13 ++-- Sources/SendGridKit/Models/EmailContent.swift | 11 ++-- Sources/SendGridKit/Models/MailSettings.swift | 23 ++++--- .../SendGridKit/Models/Personalization.swift | 24 ++++--- .../SendGridKit/Models/SendGridEmail.swift | 39 ++++++++--- .../SendGridKit/Models/SendGridError.swift | 4 +- .../SendGridKit/Models/TrackingSettings.swift | 42 +++++++----- Sources/SendGridKit/SendGridClient.swift | 10 +-- .../SendGridKit.docc/SendGridKit.md | 53 +++++++++++++++ Tests/LinuxMain.swift | 16 ----- Tests/SendGridKitTests/SendGridTestsKit.swift | 31 ++++----- 18 files changed, 260 insertions(+), 131 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 .spi.yml delete mode 100644 CHANGELOG.md create mode 100644 Sources/SendGridKit/SendGridKit.docc/SendGridKit.md delete mode 100644 Tests/LinuxMain.swift diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..bf05755 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,13 @@ +name: test +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +on: + pull_request: { types: [opened, reopened, synchronize, ready_for_review] } + push: { branches: [ main ] } + +jobs: + unit-tests: + uses: vapor/ci/.github/workflows/run-unit-tests.yml@main + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/.spi.yml b/.spi.yml new file mode 100644 index 0000000..b5ab714 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,4 @@ +version: 1 +builder: + configs: + - documentation_targets: [SendGridKit] \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index e69de29..0000000 diff --git a/Package.swift b/Package.swift index fc291f1..609aaff 100644 --- a/Package.swift +++ b/Package.swift @@ -1,23 +1,40 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.10 import PackageDescription let package = Package( name: "sendgrid-kit", platforms: [ - .macOS(.v10_15), + .macOS(.v13), ], products: [ .library(name: "SendGridKit", targets: ["SendGridKit"]), ], dependencies: [ - .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.9.0"), + .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.22.0"), ], targets: [ - .target(name: "SendGridKit", dependencies: [ - .product(name: "AsyncHTTPClient", package: "async-http-client"), - ]), - .testTarget(name: "SendGridKitTests", dependencies: [ - .target(name: "SendGridKit"), - ]) + .target( + name: "SendGridKit", + dependencies: [ + .product(name: "AsyncHTTPClient", package: "async-http-client"), + ], + swiftSettings: swiftSettings + ), + .testTarget( + name: "SendGridKitTests", + dependencies: [ + .target(name: "SendGridKit"), + ], + swiftSettings: swiftSettings + ), ] ) + +var swiftSettings: [SwiftSetting] { [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("DisableOutwardActorInference"), + .enableUpcomingFeature("StrictConcurrency"), + .enableExperimentalFeature("StrictConcurrency=complete"), +] } diff --git a/README.md b/README.md index 4893c9f..2ceba67 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,42 @@ -# SendGridKit +
+ avatar +

SendGridKit

+ + Documentation + + Team Chat + MIT License + + Continuous Integration + + + + + + Swift 5.10+ + +
+
-![Swift](http://img.shields.io/badge/swift-5.6-brightgreen.svg) +📧 SendGridKit is a Swift package used to communicate with the SendGrid API for Server Side Swift Apps. -SendGridKit is a Swift package used to communicate with the SendGrid API for Server Side Swift Apps. +Send simple emails, or leverage the full capabilities of [SendGrid's V3 API](https://www.twilio.com/docs/sendgrid/api-reference/mail-send/mail-send). -## Setup -Add the dependency to Package.swift: +### Getting Started -~~~~swift -dependencies: [ - ... - .package(url: "https://github.com/vapor-community/sendgrid-kit.git", from: "2.0.0") -], -targets: [ - .target(name: "App", dependencies: [ - .product(name: "SendGridKit", package: "sendgrid-kit"), - ]), -~~~~ +Use the SPM string to easily include the dependendency in your `Package.swift` file + +```swift +.package(url: "https://github.com/vapor-community/sendgrid-kit.git", from: "2.0.0") +``` + +and add it to your target's dependencies: + +```swift +.product(name: "SendGridKit", package: "sendgrid-kit") +``` + +## Overview Register the config and the provider. @@ -25,9 +45,10 @@ let httpClient = HTTPClient(...) let sendGridClient = SendGridClient(httpClient: httpClient, apiKey: "YOUR_API_KEY") ~~~~ -## Using the API +### Using the API + +You can use all of the available parameters here to build your `SendGridEmail`. -You can use all of the available parameters here to build your `SendGridEmail` Usage in a route closure would be as followed: ~~~~swift @@ -37,15 +58,16 @@ let email = SendGridEmail(...) try await sendGridClient.send(email) ~~~~ -## 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 +### Error handling + +If the request to the API failed for any reason a `SendGridError` is thrown, which 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 await sendGridClient.send(...) -} -catch let error as SendGridError { +} catch let error as SendGridError { print(error) } ~~~~ diff --git a/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift b/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift index 4109cbb..9235b9e 100644 --- a/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift +++ b/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift @@ -1,6 +1,6 @@ import Foundation -public struct AdvancedSuppressionManager: Codable { +public struct AdvancedSuppressionManager: Codable, Sendable { /// The unsubscribe group to associate with this email. public var groupId: Int @@ -19,5 +19,4 @@ public struct AdvancedSuppressionManager: Codable { case groupId = "group_id" case groupsToDisplay = "groups_to_display" } - } diff --git a/Sources/SendGridKit/Models/EmailAddress.swift b/Sources/SendGridKit/Models/EmailAddress.swift index 8aeda40..d3cbfad 100644 --- a/Sources/SendGridKit/Models/EmailAddress.swift +++ b/Sources/SendGridKit/Models/EmailAddress.swift @@ -1,7 +1,7 @@ import Foundation -public struct EmailAddress: Codable { - /// format: email +public struct EmailAddress: Codable, Sendable { + /// The email address of the person to whom you are sending an email. public var email: String /// The name of the person to whom you are sending an email. diff --git a/Sources/SendGridKit/Models/EmailAttachment.swift b/Sources/SendGridKit/Models/EmailAttachment.swift index b10d93d..a81a61f 100644 --- a/Sources/SendGridKit/Models/EmailAttachment.swift +++ b/Sources/SendGridKit/Models/EmailAttachment.swift @@ -1,11 +1,12 @@ import Foundation -public struct EmailAttachment: Codable { - +public struct EmailAttachment: Codable, Sendable { /// 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”. + /// 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. @@ -14,7 +15,10 @@ public struct EmailAttachment: Codable { /// 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. + /// 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( @@ -38,5 +42,4 @@ public struct EmailAttachment: Codable { case disposition case contentId = "content_id" } - } diff --git a/Sources/SendGridKit/Models/EmailContent.swift b/Sources/SendGridKit/Models/EmailContent.swift index d573ca5..63d3989 100644 --- a/Sources/SendGridKit/Models/EmailContent.swift +++ b/Sources/SendGridKit/Models/EmailContent.swift @@ -1,11 +1,14 @@ import Foundation -public struct EmailContent: Codable { - - /// The mime type of the content you are including in your email. For example, “text/plain” or “text/html”. +public struct EmailContent: Codable, Sendable { + /// The MIME type of the content you are including in your email. + /// + /// For example, `“text/plain”` or `“text/html”`. public var type: String - /// The actual content of the specified mime type that you are including in your email. minLength 1 + /// The actual content of the specified MIME type that you are including in your email. + /// + /// > Important: The minimum length is 1. public var value: String public init( diff --git a/Sources/SendGridKit/Models/MailSettings.swift b/Sources/SendGridKit/Models/MailSettings.swift index df930ca..35b7df7 100644 --- a/Sources/SendGridKit/Models/MailSettings.swift +++ b/Sources/SendGridKit/Models/MailSettings.swift @@ -1,15 +1,21 @@ import Foundation -public struct MailSettings: Codable { - - /// 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 struct MailSettings: Codable, Sendable { + /// Allows you to bypass all unsubscribe groups and suppressions to ensure that the email is delivered to every single recipient. + /// + /// > Important: This should only be used in emergencies when it is absolutely necessary that every recipient receives your email. public var bypassListManagement: Setting? - - /// Allows you to bypass the spam report list to ensure that the email is delivered to recipients. Bounce and unsubscribe lists will still be checked; addresses on these other lists will not receive the message. + /// Allows you to bypass the spam report list to ensure that the email is delivered to recipients. + /// + /// > Note: Bounce and unsubscribe lists will still be checked; + /// addresses on these other lists will not receive the message. public var bypassSpamManagement: Setting? - /// Allows you to bypass the bounce list to ensure that the email is delivered to recipients. Spam report and unsubscribe lists will still be checked; addresses on these other lists will not receive the message. + /// Allows you to bypass the bounce list to ensure that the email is delivered to recipients. + /// + /// > Note: Spam report and unsubscribe lists will still be checked; + /// addresses on these other lists will not receive the message. public var bypassBounceManagement: Setting? /// The default footer that you would like included on every email. @@ -41,13 +47,12 @@ public struct MailSettings: Codable { } } -public struct Setting: Codable { +public struct Setting: Codable, Sendable { /// Indicates if this setting is enabled. public var enable: Bool - } -public struct Footer: Codable { +public struct Footer: Codable, Sendable { /// Indicates if this setting is enabled. public var enable: Bool diff --git a/Sources/SendGridKit/Models/Personalization.swift b/Sources/SendGridKit/Models/Personalization.swift index 215bc69..7a5d315 100644 --- a/Sources/SendGridKit/Models/Personalization.swift +++ b/Sources/SendGridKit/Models/Personalization.swift @@ -1,14 +1,19 @@ import Foundation -public struct Personalization: Codable { - - /// An array of recipients. Each object within this array may contain the name, but must always contain the email, of a recipient. +public struct Personalization: Codable, Sendable { + /// An array of recipients. + /// + /// > Important: 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. + /// An array of recipients who will receive a copy of your email. + /// + /// > Important: 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. + /// An array of recipients who will receive a blind carbon copy of your email. + /// + /// > Important: 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. @@ -17,16 +22,18 @@ public struct Personalization: Codable { /// 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". + /// 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 + /// 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. + /// A UNIX timestamp allowing you to specify when you want your email to be delivered. + /// + /// > Important: Scheduling more than 72 hours in advance is forbidden. public var sendAt: Date? public init( @@ -62,5 +69,4 @@ public struct Personalization: Codable { case dynamicTemplateData = "dynamic_template_data" case sendAt = "send_at" } - } diff --git a/Sources/SendGridKit/Models/SendGridEmail.swift b/Sources/SendGridKit/Models/SendGridEmail.swift index 64691b2..186af8f 100644 --- a/Sources/SendGridKit/Models/SendGridEmail.swift +++ b/Sources/SendGridKit/Models/SendGridEmail.swift @@ -1,8 +1,10 @@ import Foundation -public struct SendGridEmail: Codable { - - /// 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 struct SendGridEmail: Codable, Sendable { + /// 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 @@ -12,7 +14,9 @@ public struct SendGridEmail: Codable { /// An array of recipients who will receive replies and/or bounces. public var replyToList: [EmailAddress]? - /// The global, or “message level”, subject of your email. This may be overridden by personalizations[x].subject. + /// The global, or “message level”, subject of your email. + /// + /// > Note: This may be overridden by `personalizations[x].subject`. public var subject: String? /// An array in which you may specify the content of your email. @@ -21,22 +25,38 @@ public struct SendGridEmail: Codable { /// 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. + /// The ID of a template that you would like to use. + /// + /// > Note: 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 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. + /// An object containing key/value pairs of header names and the value to substitute for them. + /// + /// > Important: You must ensure these are properly encoded if they contain unicode characters. + /// + /// > Important: 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. + /// An array of category names for this message. + /// + /// > Important: 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. + /// A UNIX timestamp allowing you to specify when you want your email to be delivered. + /// + /// > Note: This may be overridden by the `personalizations[x].send_at` parameter. + /// + /// > Important: 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. + /// 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. @@ -108,5 +128,4 @@ public struct SendGridEmail: Codable { case mailSettings = "mail_settings" case trackingSettings = "tracking_settings" } - } diff --git a/Sources/SendGridKit/Models/SendGridError.swift b/Sources/SendGridKit/Models/SendGridError.swift index 26feb67..7e0766c 100644 --- a/Sources/SendGridKit/Models/SendGridError.swift +++ b/Sources/SendGridKit/Models/SendGridError.swift @@ -1,10 +1,10 @@ import Foundation -public struct SendGridError: Error, Decodable { +public struct SendGridError: Error, Decodable, Sendable { public var errors: [SendGridErrorResponse]? } -public struct SendGridErrorResponse: Decodable { +public struct SendGridErrorResponse: Decodable, Sendable { 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 index 3f7de2c..1d06306 100644 --- a/Sources/SendGridKit/Models/TrackingSettings.swift +++ b/Sources/SendGridKit/Models/TrackingSettings.swift @@ -1,13 +1,18 @@ import Foundation -public struct TrackingSettings: Codable { +public struct TrackingSettings: Codable, Sendable { /// 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. + /// 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. + /// Allows you to insert a subscription management link at the bottom of the text and HTML bodies of your email. + /// + /// > Tip: If you would like to specify the location of the link within your email, you may use the ``SubscriptionTracking/substitutionTag``. public var subscriptionTracking: SubscriptionTracking? /// Allows you to enable tracking provided by Google Analytics. @@ -31,10 +36,9 @@ public struct TrackingSettings: Codable { case subscriptionTracking = "subscription_tracking" case ganalytics } - } -public struct ClickTracking: Codable { +public struct ClickTracking: Codable, Sendable { /// Indicates if this setting is enabled. public var enable: Bool @@ -45,14 +49,15 @@ public struct ClickTracking: Codable { case enable case enableText = "enable_text" } - } -public struct OpenTracking: Codable { +public struct OpenTracking: Codable, Sendable { /// 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. + /// Allows you to specify a substitution tag that you can insert in the body of your email at a location that you desire. + /// + /// > Note: This tag will be replaced by the open tracking pixel. public var substitutionTag: String? public init( @@ -67,20 +72,28 @@ public struct OpenTracking: Codable { case enable case substitutionTag = "substitution_tag" } - } -public struct SubscriptionTracking: Codable { +public struct SubscriptionTracking: Codable, Sendable { /// 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 <% %> + /// Text to be appended to the email, with the subscription tracking link. + /// + /// > Tip: 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 <% %> + /// HTML to be appended to the email, with the subscription tracking link. + /// + /// > Tip: 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. + /// A tag that will be replaced with the unsubscribe URL. + /// + /// For example: `[unsubscribe_url]`. + /// + /// If this parameter is used, it will override both the ``SubscriptionTracking/text`` and ``SubscriptionTracking/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( @@ -103,7 +116,7 @@ public struct SubscriptionTracking: Codable { } } -public struct GoogleAnalytics: Codable { +public struct GoogleAnalytics: Codable, Sendable { /// Indicates if this setting is enabled. public var enable: Bool @@ -146,5 +159,4 @@ public struct GoogleAnalytics: Codable { case utmContent = "utm_content" case utmCampaign = "utm_campaign" } - } diff --git a/Sources/SendGridKit/SendGridClient.swift b/Sources/SendGridKit/SendGridClient.swift index 4ea154a..9dd1a2d 100644 --- a/Sources/SendGridKit/SendGridClient.swift +++ b/Sources/SendGridKit/SendGridClient.swift @@ -2,18 +2,16 @@ import Foundation import NIO import AsyncHTTPClient import NIOHTTP1 -import NIOFoundationCompat -public struct SendGridClient { - +public struct SendGridClient: Sendable { 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 + encoder.dateEncodingStrategy = .secondsSince1970 + return encoder }() private let decoder: JSONDecoder = { @@ -28,7 +26,6 @@ public struct SendGridClient { } public func send(email: SendGridEmail) async throws { - var headers = HTTPHeaders() headers.add(name: "Authorization", value: "Bearer \(apiKey)") headers.add(name: "Content-Type", value: "application/json") @@ -49,6 +46,5 @@ public struct SendGridClient { let byteBuffer = response.body ?? ByteBuffer(.init()) throw try decoder.decode(SendGridError.self, from: byteBuffer) - } } diff --git a/Sources/SendGridKit/SendGridKit.docc/SendGridKit.md b/Sources/SendGridKit/SendGridKit.docc/SendGridKit.md new file mode 100644 index 0000000..104c0c8 --- /dev/null +++ b/Sources/SendGridKit/SendGridKit.docc/SendGridKit.md @@ -0,0 +1,53 @@ +# ``SendGridKit`` + +📧 SendGridKit is a Swift package used to communicate with the SendGrid API for Server Side Swift Apps. + +## Overview + +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 SendGridKit + +let email = SendGridEmail(...) +try await sendGridClient.send(email) +``` + +### Error handling + +If the request to the API failed for any reason a ``SendGridError`` is thrown, which has an ``SendGridError/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 await sendGridClient.send(...) +} catch let error as SendGridError { + print(error) +} +``` + +## Topics + +### Essentials + +- ``SendGridClient`` +- ``SendGridEmail`` +- ``Personalization`` +- ``EmailAddress`` +- ``EmailContent`` + +### Errors + +- ``SendGridError`` diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 30e94b0..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,16 +0,0 @@ -// 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 index 69d3cba..c65ec2e 100644 --- a/Tests/SendGridKitTests/SendGridTestsKit.swift +++ b/Tests/SendGridKitTests/SendGridTestsKit.swift @@ -1,17 +1,13 @@ import XCTest import AsyncHTTPClient - @testable import SendGridKit class SendGridKitTests: XCTestCase { - - private var httpClient: HTTPClient! private var client: SendGridClient! override func setUp() { - httpClient = HTTPClient(eventLoopGroupProvider: .createNew) - + httpClient = HTTPClient(eventLoopGroupProvider: .singleton) // TODO: Replace with your API key to test! client = SendGridClient(httpClient: httpClient, apiKey: "YOUR-API-KEY") } @@ -20,20 +16,17 @@ class SendGridKitTests: XCTestCase { try await httpClient.shutdown() } - func test_sendEmail() async throws { - - // TODO: Replace to address with the email address you'd like to recieve your test email! - let personalization = Personalization(to: ["TO-ADDRESS"]) - - // TODO: Replace from address with the email address associated with your verified Sender Identity! - let email = SendGridEmail( - personalizations: [personalization], - from: "FROM-ADDRESS", - subject: "Test Email", - content: ["This email was sent using SendGridKit!"] - ) - - try await client.send(email: email) + func testSendEmail() async throws { + // TODO: Replace to address with the email address you'd like to recieve your test email + let emailAddress = EmailAddress(email: "TO-ADDRESS", name: "Test User") + // TODO: Replace from address with the email address associated with your verified Sender Identity + let fromEmailAddress = EmailAddress(email: "FROM-ADDRESS", name: "Test") + let personalization = Personalization(to: [emailAddress], subject: "Test Email") + let emailContent = EmailContent(type: "text/plain", value: "This email was sent using SendGridKit!") + let email = SendGridEmail(personalizations: [personalization], from: fromEmailAddress, content: [emailContent]) + do { + try await client.send(email: email) + } catch {} } } From c7dda565b3716d8c77277b88b264886043e3110f Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Sat, 7 Sep 2024 22:48:15 +0200 Subject: [PATCH 2/3] Fix build errors (#19) * Fix build errors --- .github/CODEOWNERS | 1 + Sources/SendGridKit/SendGridClient.swift | 3 ++- Tests/SendGridKitTests/SendGridTestsKit.swift | 21 ++++++++++++++++--- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..37a58a4 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @Andrewangeta @fpseverino \ No newline at end of file diff --git a/Sources/SendGridKit/SendGridClient.swift b/Sources/SendGridKit/SendGridClient.swift index 9dd1a2d..53b1e7a 100644 --- a/Sources/SendGridKit/SendGridClient.swift +++ b/Sources/SendGridKit/SendGridClient.swift @@ -1,7 +1,8 @@ -import Foundation +@preconcurrency import Foundation import NIO import AsyncHTTPClient import NIOHTTP1 +import NIOFoundationCompat public struct SendGridClient: Sendable { let apiURL = "https://api.sendgrid.com/v3/mail/send" diff --git a/Tests/SendGridKitTests/SendGridTestsKit.swift b/Tests/SendGridKitTests/SendGridTestsKit.swift index c65ec2e..1f9c17f 100644 --- a/Tests/SendGridKitTests/SendGridTestsKit.swift +++ b/Tests/SendGridKitTests/SendGridTestsKit.swift @@ -18,12 +18,27 @@ class SendGridKitTests: XCTestCase { func testSendEmail() async throws { // TODO: Replace to address with the email address you'd like to recieve your test email - let emailAddress = EmailAddress(email: "TO-ADDRESS", name: "Test User") + let emailAddress = EmailAddress("TO-ADDRESS") // TODO: Replace from address with the email address associated with your verified Sender Identity let fromEmailAddress = EmailAddress(email: "FROM-ADDRESS", name: "Test") + let personalization = Personalization(to: [emailAddress], subject: "Test Email") - let emailContent = EmailContent(type: "text/plain", value: "This email was sent using SendGridKit!") - let email = SendGridEmail(personalizations: [personalization], from: fromEmailAddress, content: [emailContent]) + + let attachment = EmailAttachment( + content: "Hello, World!".data(using: .utf8)!.base64EncodedString(), + type: "text/plain", + filename: "hello.txt", + disposition: "attachment" + ) + + let emailContent = EmailContent("This email was sent using SendGridKit!") + + let email = SendGridEmail( + personalizations: [personalization], + from: fromEmailAddress, + content: [emailContent], + attachments: [attachment] + ) do { try await client.send(email: email) From c00b2919afd4671b387a62b0d550e1139a58f691 Mon Sep 17 00:00:00 2001 From: Francesco Paolo Severino <96546612+fpseverino@users.noreply.github.com> Date: Sat, 21 Sep 2024 10:57:14 +0200 Subject: [PATCH 3/3] Swift 6 update (#20) - Update package to Swift 6.0 - Switch from XCTest to Swift Testing - Add more test coverage - Update to latest AsyncHTTPClient APIs - Add support for EU regional subusers - Improve DocC --- .spi.yml | 3 +- Package.swift | 11 +--- README.md | 2 +- .../Models/AdvancedSuppressionManager.swift | 12 ++-- .../SendGridKit/Models/EmailAttachment.swift | 27 +++++--- .../SendGridKit/Models/SendGridEmail.swift | 29 +++++---- .../SendGridKit/Models/SendGridError.swift | 8 +++ Sources/SendGridKit/SendGridClient.swift | 35 +++++----- Tests/SendGridKitTests/SendGridTestsKit.swift | 64 ++++++++++++++----- 9 files changed, 124 insertions(+), 67 deletions(-) diff --git a/.spi.yml b/.spi.yml index b5ab714..8a3fa63 100644 --- a/.spi.yml +++ b/.spi.yml @@ -1,4 +1,5 @@ version: 1 builder: configs: - - documentation_targets: [SendGridKit] \ No newline at end of file + - documentation_targets: [SendGridKit] + swift_version: 6.0 \ No newline at end of file diff --git a/Package.swift b/Package.swift index 609aaff..d22cae6 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,10 @@ -// swift-tools-version:5.10 +// swift-tools-version:6.0 import PackageDescription let package = Package( name: "sendgrid-kit", platforms: [ - .macOS(.v13), + .macOS(.v14), ], products: [ .library(name: "SendGridKit", targets: ["SendGridKit"]), @@ -32,9 +32,4 @@ let package = Package( var swiftSettings: [SwiftSetting] { [ .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("ConciseMagicFile"), - .enableUpcomingFeature("ForwardTrailingClosures"), - .enableUpcomingFeature("DisableOutwardActorInference"), - .enableUpcomingFeature("StrictConcurrency"), - .enableExperimentalFeature("StrictConcurrency=complete"), -] } +] } \ No newline at end of file diff --git a/README.md b/README.md index 2ceba67..728c175 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - Swift 5.10+ + Swift 6.0+
diff --git a/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift b/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift index 9235b9e..575abb1 100644 --- a/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift +++ b/Sources/SendGridKit/Models/AdvancedSuppressionManager.swift @@ -2,21 +2,25 @@ import Foundation public struct AdvancedSuppressionManager: Codable, Sendable { /// The unsubscribe group to associate with this email. - public var groupId: Int + /// + /// See the Suppressions API to manage unsubscribe group IDs. + public var groupID: Int /// An array containing the unsubscribe groups that you would like to be displayed on the unsubscribe preferences page. + /// + /// This page is displayed in the recipient's browser when they click the unsubscribe link in your message. public var groupsToDisplay: [String]? public init( - groupId: Int, + groupID: Int, groupsToDisplay: [String]? = nil ) { - self.groupId = groupId + self.groupID = groupID self.groupsToDisplay = groupsToDisplay } private enum CodingKeys: String, CodingKey { - case groupId = "group_id" + case groupID = "group_id" case groupsToDisplay = "groups_to_display" } } diff --git a/Sources/SendGridKit/Models/EmailAttachment.swift b/Sources/SendGridKit/Models/EmailAttachment.swift index a81a61f..8e13788 100644 --- a/Sources/SendGridKit/Models/EmailAttachment.swift +++ b/Sources/SendGridKit/Models/EmailAttachment.swift @@ -6,33 +6,42 @@ public struct EmailAttachment: Codable, Sendable { /// The MIME type of the content you are attaching. /// - /// For example, `“text/plain”` or `“text/html”`. + /// For example, `image/jpeg`, `text/html` or `application/pdf`. public var type: String? - /// The filename of the attachment. + /// The attachment's filename, including the file extension. 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 attachment's content-disposition specifies how you would like the attachment to be displayed. + /// + /// For example, inline results in the attached file being displayed automatically within the message + /// while attachment results in the attached file requiring some action to be taken before it is displayed + /// such as opening or downloading the file. + public var disposition: Disposition? + + public enum Disposition: String, Codable, Sendable { + case inline + case attachment + } /// 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 var contentID: String? public init( content: String, type: String? = nil, filename: String, - disposition: String? = nil, - contentId: String? = nil + disposition: Disposition? = nil, + contentID: String? = nil ) { self.content = content self.type = type self.filename = filename self.disposition = disposition - self.contentId = contentId + self.contentID = contentID } private enum CodingKeys: String, CodingKey { @@ -40,6 +49,6 @@ public struct EmailAttachment: Codable, Sendable { case type case filename case disposition - case contentId = "content_id" + case contentID = "content_id" } } diff --git a/Sources/SendGridKit/Models/SendGridEmail.swift b/Sources/SendGridKit/Models/SendGridEmail.swift index 186af8f..bdc57d6 100644 --- a/Sources/SendGridKit/Models/SendGridEmail.swift +++ b/Sources/SendGridKit/Models/SendGridEmail.swift @@ -11,12 +11,19 @@ public struct SendGridEmail: Codable, Sendable { public var replyTo: EmailAddress? - /// An array of recipients who will receive replies and/or bounces. + /// An array of recipients to whom replies will be sent. + /// + /// Each object in this array must contain a recipient's email address. + /// Each object in the array may optionally contain a recipient's name. + /// You can use either the `reply_to property` or `reply_to_list` property but not both. public var replyToList: [EmailAddress]? - /// The global, or “message level”, subject of your email. + /// The global or _message level_ subject of your email. + /// + /// Subject lines set in personalizations objects will override this global subject line. + /// See line length limits specified in RFC 2822 for guidance on subject line character limits. /// - /// > Note: This may be overridden by `personalizations[x].subject`. + /// > Note: Min length: 1. public var subject: String? /// An array in which you may specify the content of your email. @@ -29,7 +36,7 @@ public struct SendGridEmail: Codable, Sendable { /// /// > Note: 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? + public var templateID: String? /// An object containing key/value pairs of header names and the value to substitute for them. /// @@ -57,7 +64,7 @@ public struct SendGridEmail: Codable, Sendable { /// /// 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? + public var batchID: String? /// An object allowing you to specify how to handle unsubscribes. public var asm: AdvancedSuppressionManager? @@ -79,12 +86,12 @@ public struct SendGridEmail: Codable, Sendable { subject: String? = nil, content: [EmailContent]? = nil, attachments: [EmailAttachment]? = nil, - templateId: String? = nil, + templateID: String? = nil, headers: [String: String]? = nil, categories: [String]? = nil, customArgs: [String: String]? = nil, sendAt: Date? = nil, - batchId: String? = nil, + batchID: String? = nil, asm: AdvancedSuppressionManager? = nil, ipPoolName: String? = nil, mailSettings: MailSettings? = nil, @@ -97,12 +104,12 @@ public struct SendGridEmail: Codable, Sendable { self.subject = subject self.content = content self.attachments = attachments - self.templateId = templateId + self.templateID = templateID self.headers = headers self.categories = categories self.customArgs = customArgs self.sendAt = sendAt - self.batchId = batchId + self.batchID = batchID self.asm = asm self.ipPoolName = ipPoolName self.mailSettings = mailSettings @@ -117,12 +124,12 @@ public struct SendGridEmail: Codable, Sendable { case subject case content case attachments - case templateId = "template_id" + case templateID = "template_id" case headers case categories case customArgs = "custom_args" case sendAt = "send_at" - case batchId = "batch_id" + case batchID = "batch_id" case asm case ipPoolName = "ip_pool_name" case mailSettings = "mail_settings" diff --git a/Sources/SendGridKit/Models/SendGridError.swift b/Sources/SendGridKit/Models/SendGridError.swift index 7e0766c..64a1fdf 100644 --- a/Sources/SendGridKit/Models/SendGridError.swift +++ b/Sources/SendGridKit/Models/SendGridError.swift @@ -2,10 +2,18 @@ import Foundation public struct SendGridError: Error, Decodable, Sendable { public var errors: [SendGridErrorResponse]? + + /// When applicable, this property value will be an error ID. + public var id: String? } public struct SendGridErrorResponse: Decodable, Sendable { + /// An error message. public var message: String? + + /// When applicable, this property value will be the field that generated the error. public var field: String? + + /// When applicable, this property value will be helper text or a link to documentation to help you troubleshoot the error. public var help: String? } diff --git a/Sources/SendGridKit/SendGridClient.swift b/Sources/SendGridKit/SendGridClient.swift index 53b1e7a..21a8a2c 100644 --- a/Sources/SendGridKit/SendGridClient.swift +++ b/Sources/SendGridKit/SendGridClient.swift @@ -1,11 +1,11 @@ -@preconcurrency import Foundation +import Foundation import NIO import AsyncHTTPClient import NIOHTTP1 import NIOFoundationCompat public struct SendGridClient: Sendable { - let apiURL = "https://api.sendgrid.com/v3/mail/send" + let apiURL: String let httpClient: HTTPClient let apiKey: String @@ -21,31 +21,34 @@ public struct SendGridClient: Sendable { return decoder }() - public init(httpClient: HTTPClient, apiKey: String) { + /// Initialize a new `SendGridClient` + /// + /// - Parameters: + /// - httpClient: The `HTTPClient` to use for sending requests + /// - apiKey: The SendGrid API key + /// - forEU: Whether to use the API endpoint for global users and subusers or for EU regional subusers + public init(httpClient: HTTPClient, apiKey: String, forEU: Bool = false) { self.httpClient = httpClient self.apiKey = apiKey + self.apiURL = forEU ? "https://api.eu.sendgrid.com/v3/mail/send" : "https://api.sendgrid.com/v3/mail/send" } public func send(email: SendGridEmail) async throws { var headers = HTTPHeaders() headers.add(name: "Authorization", value: "Bearer \(apiKey)") headers.add(name: "Content-Type", value: "application/json") + + var request = HTTPClientRequest(url: apiURL) + request.method = .POST + request.headers = headers + request.body = try HTTPClientRequest.Body.bytes(encoder.encode(email)) - let response = try await httpClient.execute( - request: .init( - url: apiURL, - method: .POST, - headers: headers, - body: .data(encoder.encode(email)) - ) - ).get() + let response = try await httpClient.execute(request, timeout: .seconds(30)) // If the request was accepted, simply return - guard response.status != .ok && response.status != .accepted else { return } + if (200...299).contains(response.status.code) { return } - // JSONDecoder will handle empty body by throwing decoding error - let byteBuffer = response.body ?? ByteBuffer(.init()) - - throw try decoder.decode(SendGridError.self, from: byteBuffer) + // JSONDecoder will handle empty body by throwing decoding error + throw try await decoder.decode(SendGridError.self, from: response.body.collect(upTo: 1024 * 1024)) } } diff --git a/Tests/SendGridKitTests/SendGridTestsKit.swift b/Tests/SendGridKitTests/SendGridTestsKit.swift index 1f9c17f..972fa94 100644 --- a/Tests/SendGridKitTests/SendGridTestsKit.swift +++ b/Tests/SendGridKitTests/SendGridTestsKit.swift @@ -1,22 +1,16 @@ -import XCTest +import Testing import AsyncHTTPClient @testable import SendGridKit -class SendGridKitTests: XCTestCase { - private var httpClient: HTTPClient! - private var client: SendGridClient! +struct SendGridKitTests { + var client: SendGridClient - override func setUp() { - httpClient = HTTPClient(eventLoopGroupProvider: .singleton) - // TODO: Replace with your API key to test! - client = SendGridClient(httpClient: httpClient, apiKey: "YOUR-API-KEY") - } - - override func tearDown() async throws { - try await httpClient.shutdown() + init() { + // TODO: Replace with a valid API key to test + client = SendGridClient(httpClient: HTTPClient.shared, apiKey: "YOUR-API-KEY") } - func testSendEmail() async throws { + @Test func sendEmail() async throws { // TODO: Replace to address with the email address you'd like to recieve your test email let emailAddress = EmailAddress("TO-ADDRESS") // TODO: Replace from address with the email address associated with your verified Sender Identity @@ -28,20 +22,56 @@ class SendGridKitTests: XCTestCase { content: "Hello, World!".data(using: .utf8)!.base64EncodedString(), type: "text/plain", filename: "hello.txt", - disposition: "attachment" + disposition: .attachment ) let emailContent = EmailContent("This email was sent using SendGridKit!") + let setting = Setting(enable: true) + let mailSettings = MailSettings( + bypassListManagement: setting, + bypassSpamManagement: setting, + bypassBounceManagement: setting, + footer: Footer(enable: true, text: "footer", html: "footer"), + sandboxMode: setting + ) + + let trackingSettings = TrackingSettings( + clickTracking: ClickTracking(enable: true, enableText: true), + openTracking: OpenTracking(enable: true, substitutionTag: "open_tracking"), + subscriptionTracking: SubscriptionTracking( + enable: true, + text: "sub_text", + html: "sub_html", + substitutionTag: "sub_tag" + ), + ganalytics: GoogleAnalytics( + enable: true, + utmSource: "utm_source", + utmMedium: "utm_medium", + utmTerm: "utm_term", + utmContent: "utm_content", + utmCampaign: "utm_campaign" + ) + ) + + let asm = AdvancedSuppressionManager(groupID: 21, groupsToDisplay: ["group1", "group2"]) + let email = SendGridEmail( personalizations: [personalization], from: fromEmailAddress, content: [emailContent], - attachments: [attachment] + attachments: [attachment], + asm: asm, + mailSettings: mailSettings, + trackingSettings: trackingSettings ) - do { + try await withKnownIssue { try await client.send(email: email) - } catch {} + } when: { + // TODO: Replace with `false` when you have a valid API key + true + } } }