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 @@
-
+
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
+ }
}
}