Skip to content

Commit f2caa82

Browse files
authored
Merge pull request #9 from vamsii777/feature/broadcasts
Add Resend Broadcast API support
2 parents 3e48267 + db6e28a commit f2caa82

File tree

11 files changed

+522
-1
lines changed

11 files changed

+522
-1
lines changed

README.md

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,63 @@ let contactId = try await resendClient.contacts.create(audienceId: audience.id,
129129
subscriptionStatus: true)
130130
~~~~
131131

132+
### Managing Broadcasts
133+
134+
Access the `BroadcastClient` for managing broadcasts via the API. Broadcasts allow you to send emails to entire audiences. Refer to the [Resend Broadcast API](https://resend.com/docs/api-reference/broadcasts) for complete details.
135+
136+
#### Creating a Broadcast
137+
~~~~swift
138+
let broadcast = BroadcastCreate(
139+
audienceId: audience.id,
140+
from: EmailAddress(email: "sender@example.com", name: "Sender Name"),
141+
subject: "Welcome to our newsletter",
142+
replyTo: [
143+
EmailAddress(email: "support@example.com", name: "Support Team")
144+
],
145+
html: "<h1>Welcome {{{FIRST_NAME|there}}}!</h1><p>Thank you for subscribing.</p>",
146+
text: "Welcome! Thank you for subscribing.",
147+
name: "Welcome Newsletter"
148+
)
149+
let broadcastId = try await resendClient.broadcasts.create(broadcast: broadcast)
150+
~~~~
151+
152+
#### Listing Broadcasts
153+
~~~~swift
154+
let broadcasts = try await resendClient.broadcasts.list()
155+
for broadcast in broadcasts.data {
156+
print("Broadcast: \(broadcast.id) - Status: \(broadcast.status)")
157+
}
158+
~~~~
159+
160+
#### Getting Broadcast Details
161+
~~~~swift
162+
let broadcast = try await resendClient.broadcasts.get(broadcastId: broadcastId)
163+
print("Broadcast subject: \(broadcast.subject)")
164+
print("From: \(broadcast.from.name) <\(broadcast.from.email)>")
165+
~~~~
166+
167+
#### Updating a Broadcast
168+
~~~~swift
169+
let update = BroadcastUpdate(
170+
id: broadcastId,
171+
subject: "Updated Subject",
172+
html: "<h1>Updated Content</h1>"
173+
)
174+
let updatedBroadcast = try await resendClient.broadcasts.update(update: update)
175+
~~~~
176+
177+
#### Sending a Broadcast
178+
~~~~swift
179+
let sentBroadcast = try await resendClient.broadcasts.send(broadcastId: broadcastId)
180+
print("Broadcast sent with ID: \(sentBroadcast.id)")
181+
~~~~
182+
183+
#### Deleting a Broadcast
184+
~~~~swift
185+
let deletedBroadcast = try await resendClient.broadcasts.delete(broadcastId: broadcastId)
186+
print("Broadcast deleted: \(deletedBroadcast.id)")
187+
~~~~
188+
132189
## Error handling
133190
If a request to the API fails for any reason, a `ResendError` is thrown. Ensure you catch errors like any other throwing function.
134191

@@ -152,7 +209,7 @@ catch let error as ResendError {
152209
- [x] Contacts
153210
- [x] Domains
154211
- [x] API Keys
155-
- [ ] Broadcasts
212+
- [x] Broadcasts
156213

157214
## License
158215

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import Foundation
2+
3+
public struct BroadcastCreate: Encodable {
4+
/// The ID of the audience you want to send to.
5+
public var audienceId: String
6+
/// Sender email address with optional friendly name.
7+
public var from: EmailAddress
8+
/// Email subject.
9+
public var subject: String
10+
/// Reply-to email addresses with optional friendly names.
11+
public var replyTo: [EmailAddress]?
12+
/// The HTML version of the message.
13+
public var html: String?
14+
/// The plain text version of the message.
15+
public var text: String?
16+
/// The friendly name of the broadcast. Only used for internal reference.
17+
public var name: String?
18+
19+
public init(audienceId: String, from: EmailAddress, subject: String, replyTo: [EmailAddress]? = nil, html: String? = nil, text: String? = nil, name: String? = nil) {
20+
self.audienceId = audienceId
21+
self.from = from
22+
self.subject = subject
23+
self.replyTo = replyTo
24+
self.html = html
25+
self.text = text
26+
self.name = name
27+
}
28+
29+
private enum CodingKeys: String, CodingKey {
30+
case audienceId = "audience_id"
31+
case from
32+
case subject
33+
case replyTo = "reply_to"
34+
case html
35+
case text
36+
case name
37+
}
38+
39+
public func encode(to encoder: Encoder) throws {
40+
var container = encoder.container(keyedBy: CodingKeys.self)
41+
try container.encode(audienceId, forKey: .audienceId)
42+
try container.encode(from.string, forKey: .from)
43+
try container.encode(subject, forKey: .subject)
44+
try container.encodeIfPresent(replyTo?.stringArray, forKey: .replyTo)
45+
try container.encodeIfPresent(html, forKey: .html)
46+
try container.encodeIfPresent(text, forKey: .text)
47+
try container.encodeIfPresent(name, forKey: .name)
48+
}
49+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Foundation
2+
3+
public struct BroadcastUpdate: Encodable {
4+
public var id: String
5+
public var audienceId: String?
6+
public var from: EmailAddress?
7+
public var subject: String?
8+
public var replyTo: [EmailAddress]?
9+
public var html: String?
10+
public var text: String?
11+
public var name: String?
12+
13+
public init(id: String, audienceId: String? = nil, from: EmailAddress? = nil, subject: String? = nil, replyTo: [EmailAddress]? = nil, html: String? = nil, text: String? = nil, name: String? = nil) {
14+
self.id = id
15+
self.audienceId = audienceId
16+
self.from = from
17+
self.subject = subject
18+
self.replyTo = replyTo
19+
self.html = html
20+
self.text = text
21+
self.name = name
22+
}
23+
24+
private enum CodingKeys: String, CodingKey {
25+
case id
26+
case audienceId = "audience_id"
27+
case from
28+
case subject
29+
case replyTo = "reply_to"
30+
case html
31+
case text
32+
case name
33+
}
34+
35+
public func encode(to encoder: Encoder) throws {
36+
var container = encoder.container(keyedBy: CodingKeys.self)
37+
try container.encode(id, forKey: .id)
38+
try container.encodeIfPresent(audienceId, forKey: .audienceId)
39+
try container.encodeIfPresent(from?.string, forKey: .from)
40+
try container.encodeIfPresent(subject, forKey: .subject)
41+
try container.encodeIfPresent(replyTo?.stringArray, forKey: .replyTo)
42+
try container.encodeIfPresent(html, forKey: .html)
43+
try container.encodeIfPresent(text, forKey: .text)
44+
try container.encodeIfPresent(name, forKey: .name)
45+
}
46+
}

Sources/Resend/Models/Request/Email/EmailSchedule.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,19 @@ extension EmailSchedule: ExpressibleByStringLiteral {
2525
/// Custom Codable implementation to convert `Date` object to proper date format
2626
/// Date format shoudl be ISO 8601
2727
extension EmailSchedule: Codable {
28+
public init(from decoder: Decoder) throws {
29+
let container = try decoder.singleValueContainer()
30+
31+
// Try to decode as Date first
32+
if let date = try? container.decode(Date.self) {
33+
self = .date(date)
34+
} else if let string = try? container.decode(String.self) {
35+
self = .string(string)
36+
} else {
37+
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expected Date or String for EmailSchedule")
38+
}
39+
}
40+
2841
public func encode(to encoder: Encoder) throws {
2942
var container = encoder.singleValueContainer()
3043

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import Foundation
2+
3+
public struct BroadcastCreateResponse: Decodable {
4+
/// Id of the created broadcast.
5+
public var id: String
6+
7+
public init(id: String) {
8+
self.id = id
9+
}
10+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import Foundation
2+
3+
public struct BroadcastGetResponse: Decodable {
4+
public var id: String
5+
public var name: String?
6+
public var audienceId: String
7+
public var from: EmailAddress
8+
public var subject: String
9+
public var replyTo: [EmailAddress]?
10+
public var previewText: String?
11+
public var status: String
12+
public var createdAt: Date
13+
public var scheduledAt: Date?
14+
public var sentAt: Date?
15+
16+
private enum CodingKeys: String, CodingKey {
17+
case id
18+
case name
19+
case audienceId = "audience_id"
20+
case from
21+
case subject
22+
case replyTo = "reply_to"
23+
case previewText = "preview_text"
24+
case status
25+
case createdAt = "created_at"
26+
case scheduledAt = "scheduled_at"
27+
case sentAt = "sent_at"
28+
}
29+
30+
public init(from decoder: Decoder) throws {
31+
let container = try decoder.container(keyedBy: CodingKeys.self)
32+
id = try container.decode(String.self, forKey: .id)
33+
name = try container.decodeIfPresent(String.self, forKey: .name)
34+
audienceId = try container.decode(String.self, forKey: .audienceId)
35+
36+
let fromString = try container.decode(String.self, forKey: .from)
37+
from = EmailAddress(from: fromString)
38+
39+
subject = try container.decode(String.self, forKey: .subject)
40+
41+
if let replyToStrings = try container.decodeIfPresent([String].self, forKey: .replyTo) {
42+
replyTo = replyToStrings.map { EmailAddress(from: $0) }
43+
} else {
44+
replyTo = nil
45+
}
46+
47+
previewText = try container.decodeIfPresent(String.self, forKey: .previewText)
48+
status = try container.decode(String.self, forKey: .status)
49+
createdAt = try container.decode(Date.self, forKey: .createdAt)
50+
scheduledAt = try container.decodeIfPresent(Date.self, forKey: .scheduledAt)
51+
sentAt = try container.decodeIfPresent(Date.self, forKey: .sentAt)
52+
}
53+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import Foundation
2+
3+
public struct BroadcastListResponse: Decodable {
4+
public var data: [BroadcastSummary]
5+
}
6+
7+
public struct BroadcastSummary: Decodable {
8+
public var id: String
9+
public var audienceId: String
10+
public var status: String
11+
public var createdAt: Date
12+
public var scheduledAt: EmailSchedule?
13+
public var sentAt: Date?
14+
15+
private enum CodingKeys: String, CodingKey {
16+
case id
17+
case audienceId = "audience_id"
18+
case status
19+
case createdAt = "created_at"
20+
case scheduledAt = "scheduled_at"
21+
case sentAt = "sent_at"
22+
}
23+
}

Sources/Resend/Resend/APIPath.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ enum APIPath {
3535
case domainUpdate(domainId: String)
3636
case domainList
3737
case domainDelete(domainId: String)
38+
case broadcastCreate
39+
case broadcastGet(broadcastId: String)
40+
case broadcastUpdate(broadcastId: String)
41+
case broadcastSend(broadcastId: String)
42+
case broadcastDelete(broadcastId: String)
43+
case broadcastList
3844

3945
private static func path(of path: String) -> String {
4046
APIPath.apiURL + path
@@ -86,6 +92,18 @@ enum APIPath {
8692
return path(of: "/domains")
8793
case .domainDelete(let domainId):
8894
return path(of: "/domains/\(domainId)")
95+
case .broadcastCreate:
96+
return path(of: "/broadcasts")
97+
case .broadcastGet(let broadcastId):
98+
return path(of: "/broadcasts/\(broadcastId)")
99+
case .broadcastUpdate(let broadcastId):
100+
return path(of: "/broadcasts/\(broadcastId)")
101+
case .broadcastSend(let broadcastId):
102+
return path(of: "/broadcasts/\(broadcastId)/send")
103+
case .broadcastDelete(let broadcastId):
104+
return path(of: "/broadcasts/\(broadcastId)")
105+
case .broadcastList:
106+
return path(of: "/broadcasts")
89107
}
90108
}
91109
}

0 commit comments

Comments
 (0)