Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift 6 update #20

Merged
merged 7 commits into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .spi.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
version: 1
builder:
configs:
- documentation_targets: [SendGridKit]
- documentation_targets: [SendGridKit]
swift_version: 6.0
11 changes: 3 additions & 8 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -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"]),
Expand Down Expand Up @@ -32,9 +32,4 @@ let package = Package(

var swiftSettings: [SwiftSetting] { [
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("ConciseMagicFile"),
fpseverino marked this conversation as resolved.
Show resolved Hide resolved
.enableUpcomingFeature("ForwardTrailingClosures"),
.enableUpcomingFeature("DisableOutwardActorInference"),
.enableUpcomingFeature("StrictConcurrency"),
.enableExperimentalFeature("StrictConcurrency=complete"),
] }
] }
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<img src="https://img.shields.io/codecov/c/github/vapor-community/sendgrid-kit?style=plastic&logo=codecov&label=codecov">
</a>
<a href="https://swift.org">
<img src="https://design.vapor.codes/images/swift510up.svg" alt="Swift 5.10+">
<img src="https://design.vapor.codes/images/swift60up.svg" alt="Swift 6.0+">
</a>
</div>
<br>
Expand Down
12 changes: 8 additions & 4 deletions Sources/SendGridKit/Models/AdvancedSuppressionManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
MahdiBM marked this conversation as resolved.
Show resolved Hide resolved
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"
}
}
27 changes: 18 additions & 9 deletions Sources/SendGridKit/Models/EmailAttachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,49 @@ 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 {
case content
case type
case filename
case disposition
case contentId = "content_id"
case contentID = "content_id"
}
}
29 changes: 18 additions & 11 deletions Sources/SendGridKit/Models/SendGridEmail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
///
Expand Down Expand Up @@ -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?
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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"
Expand Down
8 changes: 8 additions & 0 deletions Sources/SendGridKit/Models/SendGridError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
}
35 changes: 19 additions & 16 deletions Sources/SendGridKit/SendGridClient.swift
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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"
}

MahdiBM marked this conversation as resolved.
Show resolved Hide resolved
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))
}
}
64 changes: 47 additions & 17 deletions Tests/SendGridKitTests/SendGridTestsKit.swift
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
import XCTest
import Testing
import AsyncHTTPClient
fpseverino marked this conversation as resolved.
Show resolved Hide resolved
@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
Expand All @@ -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: "<strong>footer</strong>"),
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: "<strong>sub_html</strong>",
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
}
}
MahdiBM marked this conversation as resolved.
Show resolved Hide resolved
}
Loading