Skip to content
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
8 changes: 4 additions & 4 deletions MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ Version 2.0 is a complete rewrite with:

**Before (v1.x):**
```swift
.package(url: "https://github.com/yourusername/Resend.git", from: "1.0.0")
.package(url: "https://github.com/devswiftzone/Resend.git", from: "1.0.0")

// In target dependencies
.product(name: "Resend", package: "Resend")
```

**After (v2.0):**
```swift
.package(url: "https://github.com/yourusername/Resend.git", from: "2.0.0")
.package(url: "https://github.com/devswiftzone/Resend.git", from: "2.0.0")

// For iOS/macOS apps
.product(name: "Resend", package: "Resend")
Expand Down Expand Up @@ -273,7 +273,7 @@ let cancelled = try await resend.email.cancel(id: "email_id")

```swift
// Update version
.package(url: "https://github.com/yourusername/Resend.git", from: "2.0.0")
.package(url: "https://github.com/devswiftzone/Resend.git", from: "2.0.0")

// For Vapor apps, change import
.product(name: "ResendVapor", package: "Resend") // was "Resend"
Expand Down Expand Up @@ -409,7 +409,7 @@ public func configure(_ app: Application) throws {
If you encounter issues, you can rollback to v1.x:

```swift
.package(url: "https://github.com/yourusername/Resend.git", from: "1.0.0")
.package(url: "https://github.com/devswiftzone/Resend.git", from: "1.0.0")
```

However, note that v1.x is no longer maintained and has limited functionality.
16 changes: 15 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,21 @@ let package = Package(
.product(name: "Crypto", package: "swift-crypto"),
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
.enableExperimentalFeature("StrictConcurrency"),
]
),

// Vapor integration tests (requires Vapor dependency)
.testTarget(
name: "ResendVaporTests",
dependencies: [
"ResendVapor",
"ResendCore",
"ResendKit",
.product(name: "Vapor", package: "vapor"),
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency"),
]
),
]
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Add the package to your `Package.swift`:

```swift
dependencies: [
.package(url: "https://github.com/yourusername/Resend.git", from: "1.0.0")
.package(url: "https://github.com/devswiftzone/Resend.git", from: "1.0.0")
]
```

Expand Down Expand Up @@ -411,7 +411,7 @@ This project is licensed under the MIT License — see the LICENSE file for deta

- [Resend Website](https://resend.com)
- [Resend API Documentation](https://resend.com/docs)
- [GitHub Repository](https://github.com/yourusername/Resend)
- [GitHub Repository](https://github.com/devswiftzone/Resend)

## Acknowledgments

Expand Down
43 changes: 24 additions & 19 deletions Sources/ResendCore/Models/EmailAttachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,48 @@ import Foundation

/// A file attachment for an email message.
///
/// Attachments are included as Base64-encoded content in the email payload.
/// The maximum total attachment size is 40MB.
/// Attachments can be provided as Base64-encoded content or as a URL path
/// for the Resend API to fetch. The maximum total attachment size is 40MB.
///
/// ## Example
/// ## Examples
///
/// ```swift
/// // Base64-encoded content
/// let attachment = EmailAttachment(
/// content: "base64EncodedString",
/// filename: "document.pdf",
/// disposition: "attachment"
/// filename: "document.pdf"
/// )
///
/// // URL-based attachment
/// let attachment = EmailAttachment(
/// filename: "report.pdf",
/// path: "https://example.com/report.pdf"
/// )
/// ```
public struct EmailAttachment: Codable, Sendable {

/// The Base64 encoded content of the attachment
public var content: String
public var content: String?

/// The filename of the attachment
public var filename: String
public var filename: String?

/// Content-disposition: "attachment" (download) or "inline" (display in email body)
public var disposition: String
/// URL for the Resend API to fetch the attachment from
public var path: String?

/// MIME type of the attachment (e.g., "application/pdf")
public var type: String?

public init(
content: String,
filename: String,
disposition: String = "attachment"
content: String? = nil,
filename: String? = nil,
path: String? = nil,
type: String? = nil
) {
self.content = content
self.filename = filename
self.disposition = disposition
}

private enum CodingKeys: String, CodingKey {
case content
case filename
case disposition = "path"
self.path = path
self.type = type
}

}
4 changes: 2 additions & 2 deletions Sources/ResendCore/Models/ResendContact.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public struct ResendContact: Codable, Sendable {
public var id: String

/// Email address of the contact
public var email: String
public var email: String?

/// First name of the contact
public var firstName: String?
Expand All @@ -33,7 +33,7 @@ public struct ResendContact: Codable, Sendable {
public init(
object: String? = nil,
id: String,
email: String,
email: String?,
firstName: String? = nil,
lastName: String? = nil,
createdAt: String? = nil,
Expand Down
2 changes: 1 addition & 1 deletion Sources/ResendCore/Models/ResendRetrieveError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public struct ResendRetrieveError: Codable, Sendable, Error {
}

private enum CodingKeys: String, CodingKey {
case statusCode = "status_code"
case statusCode
case message
case name
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/ResendCore/Protocols/HTTPClientProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public protocol HTTPClientProtocol: Sendable {
}

/// Represents an HTTP request to be sent to the Resend API.
public struct HTTPRequest {
public struct HTTPRequest: Sendable {
/// The full URL for the request
public let url: String

Expand All @@ -46,7 +46,7 @@ public struct HTTPRequest {
}

/// Represents an HTTP response from the Resend API.
public struct HTTPResponse {
public struct HTTPResponse: Sendable {
/// The HTTP status code
public let statusCode: Int

Expand All @@ -68,7 +68,7 @@ public struct HTTPResponse {
}

/// HTTP methods used by the Resend API.
public enum HTTPMethod: String {
public enum HTTPMethod: String, Sendable {
case GET
case POST
case PATCH
Expand Down
28 changes: 17 additions & 11 deletions Sources/ResendCore/Protocols/ResendClientProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Foundation
///
/// Conforming types provide access to all Resend API operations through
/// specialized sub-clients for each resource type.
public protocol ResendClientProtocol {
public protocol ResendClientProtocol: Sendable {
/// Email operations (send, retrieve, schedule, cancel)
var email: EmailClientProtocol { get }

Expand All @@ -36,8 +36,8 @@ public protocol ResendClientProtocol {

/// Protocol for email operations.
///
/// Provides methods for sending, retrieving, scheduling, and canceling emails.
public protocol EmailClientProtocol {
/// Provides methods for sending, retrieving, listing, scheduling, and canceling emails.
public protocol EmailClientProtocol: Sendable {
/// Send an email
func send(email: ResendEmail) async throws -> ResendEmailResponse
/// Retrieve a sent email by ID
Expand All @@ -48,12 +48,16 @@ public protocol EmailClientProtocol {
func cancel(id: String) async throws -> ResendEmailResponse
/// Send a batch of emails in a single API call
func sendBatch(emails: [ResendEmail]) async throws -> ResendBatchResponse
/// List sent emails with cursor-based pagination
func list(limit: Int?, after: String?, before: String?) async throws -> ResendListResponse<ResendEmail>
/// List all sent emails using automatic cursor pagination
func listAll(limit: Int?) -> PaginatedSequence<ResendEmail>
}

/// Protocol for domain operations.
///
/// Provides methods for creating, verifying, and managing sending domains.
public protocol DomainClientProtocol {
public protocol DomainClientProtocol: Sendable {
/// Create a new domain for sending emails
func create(name: String, region: String?, customReturnPath: String?) async throws -> ResendDomain
/// Retrieve a domain by ID
Expand All @@ -72,8 +76,8 @@ public protocol DomainClientProtocol {

/// Protocol for API key operations.
///
/// Provides methods for creating, listing, and deleting API keys.
public protocol APIKeyClientProtocol {
/// Provides methods for creating, retrieving, listing, and deleting API keys.
public protocol APIKeyClientProtocol: Sendable {
/// Create a new API key
func create(name: String, permission: String?, domainId: String?) async throws -> ResendAPIKey
/// List API keys with cursor-based pagination
Expand All @@ -86,8 +90,8 @@ public protocol APIKeyClientProtocol {

/// Protocol for audience operations.
///
/// Provides methods for creating and managing audience groups for broadcast campaigns.
public protocol AudienceClientProtocol {
/// Provides methods for creating, updating, and managing audience groups for broadcast campaigns.
public protocol AudienceClientProtocol: Sendable {
/// Create a new audience
func create(name: String) async throws -> ResendAudience
/// Retrieve an audience by ID
Expand All @@ -96,14 +100,16 @@ public protocol AudienceClientProtocol {
func list(limit: Int?, after: String?, before: String?) async throws -> ResendListResponse<ResendAudience>
/// List all audiences using automatic cursor pagination
func listAll(limit: Int?) -> PaginatedSequence<ResendAudience>
/// Update an audience's name
func update(id: String, name: String) async throws -> ResendAudience
/// Delete an audience
func delete(id: String) async throws -> ResendDeleteResponse
}

/// Protocol for contact operations.
///
/// Provides methods for managing contacts within an audience.
public protocol ContactClientProtocol {
public protocol ContactClientProtocol: Sendable {
/// Create a new contact in an audience
func create(audienceId: String, email: String, firstName: String?, lastName: String?, unsubscribed: Bool?) async throws -> ResendContact
/// Retrieve a contact by audience ID and contact identifier
Expand All @@ -122,7 +128,7 @@ public protocol ContactClientProtocol {
///
/// Provides methods for creating and managing webhook endpoints
/// that receive event notifications from Resend.
public protocol WebhookClientProtocol {
public protocol WebhookClientProtocol: Sendable {
/// Create a new webhook endpoint
func create(endpoint: String, events: [String]) async throws -> ResendWebhook
/// Retrieve a webhook by ID
Expand All @@ -140,7 +146,7 @@ public protocol WebhookClientProtocol {
/// Protocol for broadcast operations.
///
/// Provides methods for creating and sending broadcast email campaigns to audiences.
public protocol BroadcastClientProtocol {
public protocol BroadcastClientProtocol: Sendable {
/// Create a new broadcast campaign
func create(audienceId: String, from: String, subject: String, replyTo: [String]?, html: String?, text: String?, name: String?) async throws -> ResendBroadcast
/// Retrieve a broadcast by ID
Expand Down
16 changes: 16 additions & 0 deletions Sources/ResendKit/Clients/AudienceClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ private struct CreateAudienceRequest: Encodable {
let name: String
}

private struct UpdateAudienceRequest: Encodable {
let name: String
}

final class AudienceClient: AudienceClientProtocol {
private let apiKey: String
private let httpClient: HTTPClientProtocol
Expand Down Expand Up @@ -69,6 +73,18 @@ final class AudienceClient: AudienceClientProtocol {
return try await httpClient.executeAndDecode(request, decoder: ResendClient.decoder)
}

public func update(id: String, name: String) async throws -> ResendAudience {
let body = try ResendClient.encoder.encode(UpdateAudienceRequest(name: name))
let request = ResendClient.buildRequest(
apiKey: apiKey,
baseURL: baseURL,
method: .PATCH,
path: "audiences/\(id)",
body: body
)
return try await httpClient.executeAndDecode(request, decoder: ResendClient.decoder)
}

public func delete(id: String) async throws -> ResendDeleteResponse {
let request = ResendClient.buildRequest(
apiKey: apiKey,
Expand Down
24 changes: 24 additions & 0 deletions Sources/ResendKit/Clients/EmailClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,28 @@ final class EmailClient: EmailClientProtocol {
)
return try await httpClient.executeAndDecode(request, decoder: ResendClient.decoder)
}

public func list(limit: Int?, after: String?, before: String?) async throws -> ResendListResponse<ResendEmail> {
var query: [URLQueryItem] = []
if let limit = limit { query.append(URLQueryItem(name: "limit", value: String(limit))) }
if let after = after { query.append(URLQueryItem(name: "after", value: after)) }
if let before = before { query.append(URLQueryItem(name: "before", value: before)) }

let request = ResendClient.buildRequest(
apiKey: apiKey,
baseURL: baseURL,
method: .GET,
path: "emails",
query: query
)
return try await httpClient.executeAndDecode(request, decoder: ResendClient.decoder)
}

public func listAll(limit: Int?) -> PaginatedSequence<ResendEmail> {
PaginatedSequence { cursor in
let response = try await self.list(limit: limit, after: cursor, before: nil)
let nextCursor = response.data.last?.id
return (response.data, response.hasMore, nextCursor)
}
}
}
2 changes: 1 addition & 1 deletion Sources/ResendKit/ResendClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import ResendCore
/// let domain = try await resend.domains.create(name: "example.com", region: nil, customReturnPath: nil)
/// let domains = try await resend.domains.list(limit: 10, after: nil, before: nil)
/// ```
public final class ResendClient: ResendClientProtocol {
public final class ResendClient: ResendClientProtocol, Sendable {

private let apiKey: String
private let httpClient: HTTPClientProtocol
Expand Down
2 changes: 1 addition & 1 deletion Sources/ResendKit/ResendKit.docc/ResendKit.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Add ResendKit to your `Package.swift`:

```swift
dependencies: [
.package(url: "https://github.com/yourusername/Resend.git", from: "1.0.0")
.package(url: "https://github.com/devswiftzone/Resend.git", from: "1.0.0")
]
```

Expand Down
18 changes: 14 additions & 4 deletions Sources/ResendKit/WebhookSignature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,20 @@ public enum WebhookSignature {

let expectedSignatures = signatureHeader
.split(separator: " ")
.compactMap { piece -> String? in
let parts = piece.split(separator: ",", maxSplits: 1)
guard parts.count == 2, parts[0] == "v1" else { return nil }
return String(parts[1])
.flatMap { piece -> [String] in
let token = String(piece)
if token.hasPrefix("v1=") {
return [String(token.dropFirst(3))]
}
if token.hasPrefix("v1,") {
return [String(token.dropFirst(3))]
}
return token.split(separator: ",").compactMap { part -> String? in
let s = String(part)
if s.hasPrefix("v1=") { return String(s.dropFirst(3)) }
if s.hasPrefix("v1,") { return String(s.dropFirst(3)) }
return nil
}
}

guard !expectedSignatures.isEmpty else {
Expand Down
Loading
Loading