Skip to content

Commit eecefd7

Browse files
authored
feat: Batch validation mode (#123)
1 parent eeab6c8 commit eecefd7

File tree

5 files changed

+196
-2
lines changed

5 files changed

+196
-2
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
resend (0.23.0)
4+
resend (0.24.0)
55
httparty (>= 0.21.0)
66

77
GEM

examples/batch_email.rb

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,41 @@
2929
# Send a batch of emails with Idempotency key
3030
emails = Resend::Batch.send(params, options: { idempotency_key: "af67ff1cdf3cdf1" })
3131
puts "Emails sent with Idempotency key:"
32-
puts(emails)
32+
puts(emails)
33+
34+
# Example with batch_validation: permissive
35+
# This will send valid emails and return errors for invalid ones
36+
params_with_invalid = [
37+
{
38+
"from": "[email protected]",
39+
"to": ["[email protected]"],
40+
"subject": "Valid email",
41+
"html": "<strong>This email is valid!</strong>",
42+
},
43+
{
44+
"from": "[email protected]",
45+
"to": ["invalid-email"], # Invalid email format
46+
"subject": "Invalid email",
47+
"html": "<strong>This email has an invalid recipient!</strong>",
48+
},
49+
]
50+
51+
# Send batch with permissive validation mode
52+
# This will send the valid email and return error details for the invalid one
53+
emails = Resend::Batch.send(params_with_invalid, options: { batch_validation: "permissive" })
54+
puts "\nEmails sent with permissive validation mode:"
55+
puts "Successful emails:"
56+
puts emails[:data] if emails[:data]
57+
puts "Errors:"
58+
puts emails[:errors] if emails[:errors]
59+
60+
# Send batch with strict validation mode (default behavior)
61+
# This will fail the entire batch if any email is invalid
62+
begin
63+
emails = Resend::Batch.send(params_with_invalid, options: { batch_validation: "strict" })
64+
puts "\nEmails sent with strict validation mode:"
65+
puts(emails)
66+
rescue => e
67+
puts "\nStrict validation mode failed as expected:"
68+
puts "Error: #{e.message}"
69+
end

lib/resend/batch.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,27 @@ module Resend
44
# Module responsible for wrapping Batch email sending API
55
module Batch
66
class << self
7+
# Send a batch of emails
8+
#
9+
# @param params [Array<Hash>] Array of email parameters (max 100 emails)
10+
# @param options [Hash] Additional options for the request
11+
# @option options [String] :idempotency_key Optional idempotency key
12+
# @option options [String] :batch_validation Batch validation mode: "strict" (default) or "permissive"
13+
# - "strict": Entire batch fails if any email is invalid
14+
# - "permissive": Sends valid emails and returns errors for invalid ones
15+
#
16+
# @return [Hash] Response with :data array and optional :errors array (in permissive mode)
17+
#
18+
# @example Send batch with strict validation (default)
19+
# Resend::Batch.send([
20+
# { from: "[email protected]", to: ["[email protected]"], subject: "Hello", html: "<p>Hi</p>" }
21+
# ])
22+
#
23+
# @example Send batch with permissive validation
24+
# response = Resend::Batch.send(emails, options: { batch_validation: "permissive" })
25+
# # response[:data] contains successful email IDs
26+
# # response[:errors] contains validation errors with index and message
27+
#
728
# https://resend.com/docs/api-reference/emails/send-batch-emails
829
def send(params = [], options: {})
930
path = "emails/batch"

lib/resend/request.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def initialize(path = "", body = {}, verb = "POST", options: {})
2525
}
2626

2727
set_idempotency_key
28+
set_batch_validation
2829
end
2930

3031
# Performs the HTTP call
@@ -66,6 +67,14 @@ def set_idempotency_key
6667
end
6768
end
6869

70+
def set_batch_validation
71+
# Set x-batch-validation header for batch emails
72+
# Supported values: 'strict' (default) or 'permissive'
73+
if @path == "emails/batch" && @options[:batch_validation]
74+
@headers["x-batch-validation"] = @options[:batch_validation]
75+
end
76+
end
77+
6978
def check_json!(resp)
7079
if resp.body.is_a?(Hash)
7180
JSON.parse(resp.body.to_json)

spec/batch_spec.rb

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,5 +138,132 @@
138138
}
139139
)
140140
end
141+
142+
it "does not send the x-batch-validation header when :batch_validation is not provided" do
143+
resp = {
144+
"data": [
145+
{ "id": "ae2014de-c168-4c61-8267-70d2662a1ce1" },
146+
{ "id": "faccb7a5-8a28-4e9a-ac64-8da1cc3bc1cb" }
147+
]
148+
}
149+
150+
allow(resp).to receive(:body).and_return(resp)
151+
allow(HTTParty).to receive(:send).and_return(resp)
152+
153+
Resend::Batch.send([{ from: "me" }])
154+
155+
expect(HTTParty).to have_received(:send).with(
156+
:post,
157+
"#{Resend::Request::BASE_URL}emails/batch",
158+
{
159+
headers: {
160+
"Content-Type" => "application/json",
161+
"Accept" => "application/json",
162+
"Authorization" => "Bearer re_123",
163+
"User-Agent" => "resend-ruby:#{Resend::VERSION}",
164+
},
165+
body: [{ from: "me" }].to_json
166+
}
167+
)
168+
end
169+
170+
it "sends the x-batch-validation header when :batch_validation is set to permissive" do
171+
resp = {
172+
"data": [
173+
{ "id": "ae2014de-c168-4c61-8267-70d2662a1ce1" }
174+
],
175+
"errors": [
176+
{ "index": 1, "message": "Invalid email address" }
177+
]
178+
}
179+
180+
allow(resp).to receive(:body).and_return(resp)
181+
allow(HTTParty).to receive(:send).and_return(resp)
182+
183+
Resend::Batch.send([{ from: "me" }], options: { batch_validation: "permissive" })
184+
185+
expect(HTTParty).to have_received(:send).with(
186+
:post,
187+
"#{Resend::Request::BASE_URL}emails/batch",
188+
{
189+
headers: {
190+
"Content-Type" => "application/json",
191+
"Accept" => "application/json",
192+
"Authorization" => "Bearer re_123",
193+
"User-Agent" => "resend-ruby:#{Resend::VERSION}",
194+
"x-batch-validation" => "permissive"
195+
},
196+
body: [{ from: "me" }].to_json
197+
}
198+
)
199+
end
200+
201+
it "sends the x-batch-validation header when :batch_validation is set to strict" do
202+
resp = {
203+
"data": [
204+
{ "id": "ae2014de-c168-4c61-8267-70d2662a1ce1" },
205+
{ "id": "faccb7a5-8a28-4e9a-ac64-8da1cc3bc1cb" }
206+
]
207+
}
208+
209+
allow(resp).to receive(:body).and_return(resp)
210+
allow(HTTParty).to receive(:send).and_return(resp)
211+
212+
Resend::Batch.send([{ from: "me" }], options: { batch_validation: "strict" })
213+
214+
expect(HTTParty).to have_received(:send).with(
215+
:post,
216+
"#{Resend::Request::BASE_URL}emails/batch",
217+
{
218+
headers: {
219+
"Content-Type" => "application/json",
220+
"Accept" => "application/json",
221+
"Authorization" => "Bearer re_123",
222+
"User-Agent" => "resend-ruby:#{Resend::VERSION}",
223+
"x-batch-validation" => "strict"
224+
},
225+
body: [{ from: "me" }].to_json
226+
}
227+
)
228+
end
229+
230+
it "handles response with errors array in permissive mode" do
231+
resp = {
232+
"data": [
233+
{ "id": "ae2014de-c168-4c61-8267-70d2662a1ce1" }
234+
],
235+
"errors": [
236+
{
237+
"index": 1,
238+
"message": "The 'to' field must be a valid email address"
239+
}
240+
]
241+
}
242+
243+
params = [
244+
{
245+
"from": "[email protected]",
246+
"to": ["[email protected]"],
247+
"text": "testing",
248+
"subject": "Hey",
249+
},
250+
{
251+
"from": "[email protected]",
252+
"to": ["invalid-email"],
253+
"text": "testing",
254+
"subject": "Hello",
255+
},
256+
]
257+
258+
allow_any_instance_of(Resend::Request).to receive(:perform).and_return(resp)
259+
260+
result = Resend::Batch.send(params, options: { batch_validation: "permissive" })
261+
262+
expect(result[:data].length).to eq 1
263+
expect(result[:errors]).not_to be_nil
264+
expect(result[:errors].length).to eq 1
265+
expect(result[:errors][0][:index]).to eq 1
266+
expect(result[:errors][0][:message]).to include("valid email address")
267+
end
141268
end
142269
end

0 commit comments

Comments
 (0)