resend 0.27.0.alpha.1 → 0.27.0.alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c2285e905778c2a5332f4adc94ac29f229ed02b62645c4e074febd95202d56cc
4
- data.tar.gz: 978ae7cf38ac0906c40d8af4b55aa5f0602a929bed2bc543b29500b157a94eaf
3
+ metadata.gz: 4133247b039437fb66f5b38abbe61c41d9cc85e007ae7ef7a00b865af21f9875
4
+ data.tar.gz: ab112b1b77f4f7023fe98c2ab23fa93448d1b1700d7c3ea3dbf68f78ea69e214
5
5
  SHA512:
6
- metadata.gz: 6c338e6d9b06847278ba7cd9bbd146f3d0d8012d5c614418a6ef9066c931cb73b4a270a407e74dfba7e6a5a27944c1996537c720d7108d6a838f5003a2aa2b7f
7
- data.tar.gz: 396fdd53a1e21840dcbd996735a8570ec7e5353a87b4ce86ccb311f9dfcdd2cfa0928a0883b0727423ed39d8f5bf30a6b7afa45e60743191fd29ca593307f3e9
6
+ metadata.gz: b525a8dabb6d312585fa59db58e17cf8982f3ac5603502a62046ce24211b3ba59c056361e80ac7a958d6d073cea940fece2493c320b5ac7842eed88e34b329c3
7
+ data.tar.gz: 38a6ce6faf1b907ee646bf53f28f8f0b67b7d1174e3a2d7fd075879c24a2399817d820ed8f0a2e19f50251f84a3ad7d2e0327a669f4e368e45f531cc3413b43a
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Resend
4
- module Attachments
5
- # Module for receiving email attachments API operations
6
- module Receiving
4
+ module Emails
5
+ # Module for sent email attachments API operations
6
+ module Attachments
7
7
  class << self
8
- # Retrieve a single attachment from a received email
8
+ # Retrieve a single attachment from a sent email
9
9
  #
10
10
  # @param params [Hash] Parameters for retrieving the attachment
11
11
  # @option params [String] :id The attachment ID (required)
@@ -13,7 +13,7 @@ module Resend
13
13
  # @return [Hash] The attachment object
14
14
  #
15
15
  # @example
16
- # Resend::Attachments::Receiving.get(
16
+ # Resend::Emails::Attachments.get(
17
17
  # id: "2a0c9ce0-3112-4728-976e-47ddcd16a318",
18
18
  # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
19
19
  # )
@@ -21,11 +21,11 @@ module Resend
21
21
  attachment_id = params[:id]
22
22
  email_id = params[:email_id]
23
23
 
24
- path = "emails/receiving/#{email_id}/attachments/#{attachment_id}"
24
+ path = "emails/#{email_id}/attachments/#{attachment_id}"
25
25
  Resend::Request.new(path, {}, "get").perform
26
26
  end
27
27
 
28
- # List attachments from a received email with optional pagination
28
+ # List attachments from a sent email with optional pagination
29
29
  #
30
30
  # @param params [Hash] Parameters for listing attachments
31
31
  # @option params [String] :email_id The email ID (required)
@@ -35,33 +35,31 @@ module Resend
35
35
  # @return [Hash] List of attachments with pagination info
36
36
  #
37
37
  # @example List all attachments
38
- # Resend::Attachments::Receiving.list(
38
+ # Resend::Emails::Attachments.list(
39
39
  # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
40
40
  # )
41
41
  #
42
42
  # @example List with custom limit
43
- # Resend::Attachments::Receiving.list(
43
+ # Resend::Emails::Attachments.list(
44
44
  # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
45
45
  # limit: 50
46
46
  # )
47
47
  #
48
48
  # @example List with pagination
49
- # Resend::Attachments::Receiving.list(
49
+ # Resend::Emails::Attachments.list(
50
50
  # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
51
51
  # limit: 20,
52
52
  # after: "attachment_id_123"
53
53
  # )
54
54
  def list(params = {})
55
55
  email_id = params[:email_id]
56
- path = "emails/receiving/#{email_id}/attachments"
56
+ base_path = "emails/#{email_id}/attachments"
57
57
 
58
- # Build query parameters, filtering out nil values
59
- query_params = {}
60
- query_params[:limit] = params[:limit] if params[:limit]
61
- query_params[:after] = params[:after] if params[:after]
62
- query_params[:before] = params[:before] if params[:before]
58
+ # Extract pagination parameters
59
+ pagination_params = params.slice(:limit, :after, :before)
63
60
 
64
- Resend::Request.new(path, query_params, "get").perform
61
+ path = Resend::PaginationHelper.build_paginated_path(base_path, pagination_params)
62
+ Resend::Request.new(path, {}, "get").perform
65
63
  end
66
64
  end
67
65
  end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resend
4
+ module Emails
5
+ module Receiving
6
+ # Module for received email attachments API operations
7
+ module Attachments
8
+ class << self
9
+ # Retrieve a single attachment from a received email
10
+ #
11
+ # @param params [Hash] Parameters for retrieving the attachment
12
+ # @option params [String] :id The attachment ID (required)
13
+ # @option params [String] :email_id The email ID (required)
14
+ # @return [Hash] The attachment object
15
+ #
16
+ # @example
17
+ # Resend::Emails::Receiving::Attachments.get(
18
+ # id: "2a0c9ce0-3112-4728-976e-47ddcd16a318",
19
+ # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
20
+ # )
21
+ def get(params = {})
22
+ attachment_id = params[:id]
23
+ email_id = params[:email_id]
24
+
25
+ path = "emails/receiving/#{email_id}/attachments/#{attachment_id}"
26
+ Resend::Request.new(path, {}, "get").perform
27
+ end
28
+
29
+ # List attachments from a received email with optional pagination
30
+ #
31
+ # @param params [Hash] Parameters for listing attachments
32
+ # @option params [String] :email_id The email ID (required)
33
+ # @option params [Integer] :limit Maximum number of attachments to return (1-100)
34
+ # @option params [String] :after Cursor for pagination (newer attachments)
35
+ # @option params [String] :before Cursor for pagination (older attachments)
36
+ # @return [Hash] List of attachments with pagination info
37
+ #
38
+ # @example List all attachments
39
+ # Resend::Emails::Receiving::Attachments.list(
40
+ # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c"
41
+ # )
42
+ #
43
+ # @example List with custom limit
44
+ # Resend::Emails::Receiving::Attachments.list(
45
+ # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
46
+ # limit: 50
47
+ # )
48
+ #
49
+ # @example List with pagination
50
+ # Resend::Emails::Receiving::Attachments.list(
51
+ # email_id: "4ef9a417-02e9-4d39-ad75-9611e0fcc33c",
52
+ # limit: 20,
53
+ # after: "attachment_id_123"
54
+ # )
55
+ def list(params = {})
56
+ email_id = params[:email_id]
57
+ base_path = "emails/receiving/#{email_id}/attachments"
58
+
59
+ # Extract pagination parameters
60
+ pagination_params = params.slice(:limit, :after, :before)
61
+
62
+ path = Resend::PaginationHelper.build_paginated_path(base_path, pagination_params)
63
+ Resend::Request.new(path, {}, "get").perform
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Resend
4
+ # Templates api wrapper
5
+ module Templates
6
+ class << self
7
+ # https://resend.com/docs/api-reference/templates/create-template
8
+ def create(params = {})
9
+ path = "templates"
10
+ Resend::Request.new(path, params, "post").perform
11
+ end
12
+
13
+ # https://resend.com/docs/api-reference/templates/get-template
14
+ def get(template_id = "")
15
+ path = "templates/#{template_id}"
16
+ Resend::Request.new(path, {}, "get").perform
17
+ end
18
+
19
+ # https://resend.com/docs/api-reference/templates/update-template
20
+ def update(template_id, params = {})
21
+ path = "templates/#{template_id}"
22
+ Resend::Request.new(path, params, "patch").perform
23
+ end
24
+
25
+ # https://resend.com/docs/api-reference/templates/publish-template
26
+ def publish(template_id = "")
27
+ path = "templates/#{template_id}/publish"
28
+ Resend::Request.new(path, {}, "post").perform
29
+ end
30
+
31
+ # https://resend.com/docs/api-reference/templates/duplicate-template
32
+ def duplicate(template_id = "")
33
+ path = "templates/#{template_id}/duplicate"
34
+ Resend::Request.new(path, {}, "post").perform
35
+ end
36
+
37
+ # https://resend.com/docs/api-reference/templates/list-templates
38
+ def list(params = {})
39
+ path = Resend::PaginationHelper.build_paginated_path("templates", params)
40
+ Resend::Request.new(path, {}, "get").perform
41
+ end
42
+
43
+ # https://resend.com/docs/api-reference/templates/delete-template
44
+ def remove(template_id = "")
45
+ path = "templates/#{template_id}"
46
+ Resend::Request.new(path, {}, "delete").perform
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Resend
4
- VERSION = "0.27.0.alpha.1"
4
+ VERSION = "0.27.0.alpha.2"
5
5
  end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "base64"
5
+
6
+ module Resend
7
+ # The Webhooks module provides methods for managing webhooks via the Resend API.
8
+ # Webhooks allow you to receive real-time notifications about email events.
9
+ #
10
+ # Default tolerance for timestamp validation (5 minutes)
11
+ WEBHOOK_TOLERANCE_SECONDS = 300
12
+ #
13
+ # @example Create a webhook
14
+ # Resend::Webhooks.create(
15
+ # endpoint: "https://webhook.example.com/handler",
16
+ # events: ["email.sent", "email.delivered", "email.bounced"]
17
+ # )
18
+ #
19
+ # @example List all webhooks
20
+ # Resend::Webhooks.list
21
+ #
22
+ # @example Retrieve a specific webhook
23
+ # Resend::Webhooks.get("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
24
+ #
25
+ # @example Update a webhook
26
+ # Resend::Webhooks.update(
27
+ # webhook_id: "430eed87-632a-4ea6-90db-0aace67ec228",
28
+ # endpoint: "https://new-webhook.example.com/handler",
29
+ # events: ["email.sent", "email.delivered"],
30
+ # status: "enabled"
31
+ # )
32
+ #
33
+ # @example Delete a webhook
34
+ # Resend::Webhooks.remove("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
35
+ module Webhooks
36
+ class << self
37
+ # Create a new webhook to receive real-time notifications about email events
38
+ #
39
+ # @param params [Hash] The webhook parameters
40
+ # @option params [String] :endpoint The URL where webhook events will be sent (required)
41
+ # @option params [Array<String>] :events Array of event types to subscribe to (required)
42
+ #
43
+ # @return [Hash] The webhook object containing id, object type, and signing_secret
44
+ #
45
+ # @example
46
+ # Resend::Webhooks.create(
47
+ # endpoint: "https://webhook.example.com/handler",
48
+ # events: ["email.sent", "email.delivered", "email.bounced"]
49
+ # )
50
+ def create(params = {})
51
+ path = "webhooks"
52
+ Resend::Request.new(path, params, "post").perform
53
+ end
54
+
55
+ # Retrieve a list of webhooks for the authenticated user
56
+ #
57
+ # @param params [Hash] The pagination parameters
58
+ # @option params [Integer] :limit Number of webhooks to retrieve (max: 100, min: 1)
59
+ # @option params [String] :after The ID after which to retrieve more webhooks (for pagination)
60
+ # @option params [String] :before The ID before which to retrieve more webhooks (for pagination)
61
+ #
62
+ # @return [Hash] A paginated list of webhook objects
63
+ #
64
+ # @example
65
+ # Resend::Webhooks.list
66
+ #
67
+ # @example With pagination
68
+ # Resend::Webhooks.list(limit: 20, after: "4dd369bc-aa82-4ff3-97de-514ae3000ee0")
69
+ def list(params = {})
70
+ path = Resend::PaginationHelper.build_paginated_path("webhooks", params)
71
+ Resend::Request.new(path, {}, "get").perform
72
+ end
73
+
74
+ # Retrieve a single webhook for the authenticated user
75
+ #
76
+ # @param webhook_id [String] The webhook ID
77
+ #
78
+ # @return [Hash] The webhook object with full details
79
+ #
80
+ # @example
81
+ # Resend::Webhooks.get("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
82
+ def get(webhook_id = "")
83
+ path = "webhooks/#{webhook_id}"
84
+ Resend::Request.new(path, {}, "get").perform
85
+ end
86
+
87
+ # Update an existing webhook configuration
88
+ #
89
+ # @param params [Hash] The webhook update parameters
90
+ # @option params [String] :webhook_id The webhook ID (required)
91
+ # @option params [String] :endpoint The URL where webhook events will be sent
92
+ # @option params [Array<String>] :events Array of event types to subscribe to
93
+ # @option params [String] :status The webhook status ("enabled" or "disabled")
94
+ #
95
+ # @return [Hash] The updated webhook object
96
+ #
97
+ # @example
98
+ # Resend::Webhooks.update(
99
+ # webhook_id: "430eed87-632a-4ea6-90db-0aace67ec228",
100
+ # endpoint: "https://new-webhook.example.com/handler",
101
+ # events: ["email.sent", "email.delivered"],
102
+ # status: "enabled"
103
+ # )
104
+ def update(params = {})
105
+ webhook_id = params.delete(:webhook_id)
106
+ path = "webhooks/#{webhook_id}"
107
+ Resend::Request.new(path, params, "patch").perform
108
+ end
109
+
110
+ # Remove an existing webhook
111
+ #
112
+ # @param webhook_id [String] The webhook ID
113
+ #
114
+ # @return [Hash] Confirmation object with id, object type, and deleted status
115
+ #
116
+ # @example
117
+ # Resend::Webhooks.remove("4dd369bc-aa82-4ff3-97de-514ae3000ee0")
118
+ def remove(webhook_id = "")
119
+ path = "webhooks/#{webhook_id}"
120
+ Resend::Request.new(path, {}, "delete").perform
121
+ end
122
+
123
+ # Verify a webhook payload using HMAC-SHA256 signature validation
124
+ # This validates that the webhook request came from Resend and hasn't been tampered with
125
+ #
126
+ # @param params [Hash] The webhook verification parameters
127
+ # @option params [String] :payload The raw webhook payload body (required)
128
+ # @option params [Hash] :headers The webhook headers containing svix-id, svix-timestamp,
129
+ # and svix-signature (required)
130
+ # @option params [String] :webhook_secret The signing secret from webhook creation (required)
131
+ #
132
+ # @return [Boolean] true if verification succeeds
133
+ # @raise [StandardError] If verification fails or required parameters are missing
134
+ #
135
+ # @example
136
+ # Resend::Webhooks.verify(
137
+ # payload: request.body.read,
138
+ # headers: {
139
+ # svix_id: "id_1234567890abcdefghijklmnopqrstuvwxyz",
140
+ # svix_timestamp: "1616161616",
141
+ # svix_signature: "v1,signature_here"
142
+ # },
143
+ # webhook_secret: "whsec_1234567890abcdez"
144
+ # )
145
+ def verify(params = {})
146
+ payload = params[:payload]
147
+ headers = params[:headers] || {}
148
+ webhook_secret = params[:webhook_secret]
149
+
150
+ validate_required_params(payload, headers, webhook_secret)
151
+ validate_timestamp(headers[:svix_timestamp])
152
+
153
+ signed_content = "#{headers[:svix_id]}.#{headers[:svix_timestamp]}.#{payload}"
154
+ decoded_secret = decode_secret(webhook_secret)
155
+ expected_signature = generate_signature(decoded_secret, signed_content)
156
+
157
+ verify_signature(headers[:svix_signature], expected_signature)
158
+ end
159
+
160
+ private
161
+
162
+ # Validate required parameters
163
+ def validate_required_params(payload, headers, webhook_secret)
164
+ validate_payload(payload)
165
+ validate_webhook_secret(webhook_secret)
166
+ validate_headers(headers)
167
+ end
168
+
169
+ # Validate payload is present
170
+ def validate_payload(payload)
171
+ raise "payload cannot be empty" if payload.nil? || payload.empty?
172
+ end
173
+
174
+ # Validate webhook secret is present
175
+ def validate_webhook_secret(webhook_secret)
176
+ raise "webhook_secret cannot be empty" if webhook_secret.nil? || webhook_secret.empty?
177
+ end
178
+
179
+ # Validate required headers are present
180
+ def validate_headers(headers)
181
+ raise "svix-id header is required" if headers[:svix_id].nil? || headers[:svix_id].empty?
182
+ raise "svix-timestamp header is required" if headers[:svix_timestamp].nil? || headers[:svix_timestamp].empty?
183
+ raise "svix-signature header is required" if headers[:svix_signature].nil? || headers[:svix_signature].empty?
184
+ end
185
+
186
+ # Validate timestamp to prevent replay attacks
187
+ def validate_timestamp(timestamp_header)
188
+ timestamp = timestamp_header.to_i
189
+ now = Time.now.to_i
190
+ diff = now - timestamp
191
+
192
+ return unless diff > WEBHOOK_TOLERANCE_SECONDS || diff < -WEBHOOK_TOLERANCE_SECONDS
193
+
194
+ raise "Timestamp outside tolerance window: difference of #{diff} seconds"
195
+ end
196
+
197
+ # Decode the signing secret (strip whsec_ prefix and base64 decode)
198
+ def decode_secret(webhook_secret)
199
+ secret = webhook_secret.sub(/^whsec_/, "")
200
+ Base64.strict_decode64(secret)
201
+ rescue ArgumentError => e
202
+ raise "Failed to decode webhook secret: #{e.message}"
203
+ end
204
+
205
+ # Verify signature using constant-time comparison
206
+ def verify_signature(signature_header, expected_signature)
207
+ signatures = signature_header.split(" ")
208
+ signatures.each do |sig|
209
+ parts = sig.split(",", 2)
210
+ next if parts.length != 2
211
+
212
+ received_signature = parts[1]
213
+ return true if secure_compare(expected_signature, received_signature)
214
+ end
215
+
216
+ raise "No matching signature found"
217
+ end
218
+
219
+ # Generate HMAC-SHA256 signature and return it as base64
220
+ def generate_signature(secret, content)
221
+ digest = OpenSSL::HMAC.digest("sha256", secret, content)
222
+ Base64.strict_encode64(digest)
223
+ end
224
+
225
+ # Constant-time string comparison to prevent timing attacks
226
+ #
227
+ # Note: We implement this manually for Ruby 2.7 compatibility.
228
+ # Ruby 3.0+ could use OpenSSL.fixed_length_secure_compare instead.
229
+ def secure_compare(str_a, str_b)
230
+ return false if str_a.nil? || str_b.nil? || str_a.bytesize != str_b.bytesize
231
+
232
+ bytes_a = str_a.unpack("C*")
233
+ result = 0
234
+ str_b.each_byte.with_index { |byte_b, i| result |= byte_b ^ bytes_a[i] }
235
+ result.zero?
236
+ end
237
+ end
238
+ end
239
+ end
data/lib/resend.rb CHANGED
@@ -20,9 +20,12 @@ require "resend/batch"
20
20
  require "resend/contacts"
21
21
  require "resend/domains"
22
22
  require "resend/emails"
23
+ require "resend/templates"
23
24
  require "resend/emails/receiving"
24
- require "resend/attachments/receiving"
25
+ require "resend/emails/attachments"
26
+ require "resend/emails/receiving/attachments"
25
27
  require "resend/topics"
28
+ require "resend/webhooks"
26
29
 
27
30
  # Rails
28
31
  require "resend/railtie" if defined?(Rails) && defined?(ActionMailer)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resend
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.27.0.alpha.1
4
+ version: 0.27.0.alpha.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derich Pacheco
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-10-27 00:00:00.000000000 Z
11
+ date: 2025-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httparty
@@ -47,7 +47,6 @@ files:
47
47
  - README.md
48
48
  - lib/resend.rb
49
49
  - lib/resend/api_keys.rb
50
- - lib/resend/attachments/receiving.rb
51
50
  - lib/resend/audiences.rb
52
51
  - lib/resend/batch.rb
53
52
  - lib/resend/broadcasts.rb
@@ -55,14 +54,18 @@ files:
55
54
  - lib/resend/contacts.rb
56
55
  - lib/resend/domains.rb
57
56
  - lib/resend/emails.rb
57
+ - lib/resend/emails/attachments.rb
58
58
  - lib/resend/emails/receiving.rb
59
+ - lib/resend/emails/receiving/attachments.rb
59
60
  - lib/resend/errors.rb
60
61
  - lib/resend/mailer.rb
61
62
  - lib/resend/pagination_helper.rb
62
63
  - lib/resend/railtie.rb
63
64
  - lib/resend/request.rb
65
+ - lib/resend/templates.rb
64
66
  - lib/resend/topics.rb
65
67
  - lib/resend/version.rb
68
+ - lib/resend/webhooks.rb
66
69
  homepage: https://github.com/resend/resend-ruby
67
70
  licenses:
68
71
  - MIT