resend 1.13.0 → 1.14.0

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: 02a3659fe54719cec434f3a832095f1b7ca944ace42891c84f74f295dcf6c241
4
- data.tar.gz: 36879b724cb895f867f470a56daad7b1b1747796fe9fbc4fa45ff1b184d2c6cf
3
+ metadata.gz: aedf021421daf6882e301c0fb95bc9a9e5139aeefc7df4317c2c19516da45b39
4
+ data.tar.gz: f815d39241cc898ce249b945f58b2627673d561c5546eed275d5f5337c4d666b
5
5
  SHA512:
6
- metadata.gz: 22f62c7e2ff8994689eba4ae17978f12455e6c1d98e16b3cb969c4cc36a9750909bfce85dcef5fc7d7b69f714001c46b101c15c42e2b00a03a9717f71efd50bb
7
- data.tar.gz: 95d4550369ebee3ee9a1b85fd1347ab6a606cd3a8d0ba8db062da02d1f0bb57c81063ed3073654ef185d444dabeb52e22dc1fd3027914268cbed0a559873b25f
6
+ metadata.gz: 79ee7639752c4cb1fb7d50c8aebf1959d998fb2aad158a5d6296dc38c0239cc898f6f65fd0bfce71ef5648eeadb0bf6ef17849234048270baed5896b81d9dee4
7
+ data.tar.gz: 2cab25a4b638ce11011e4fe54166677070cf104cbbca899d63d3db5a5995ce8644634ab9261c348901f5263214cc43121d3c8ba6f4eb9dac8e237baee2d1ccfc
data/README.md CHANGED
@@ -106,3 +106,43 @@ mailer = UserMailer.with(user: u).welcome_email
106
106
  mailer.deliver_now!
107
107
  # => {:id=>"b8f94710-0d84-429c-925a-22d3d8f86916", from: 'you@yourdomain.io', to: ["example2@mail.com", "example1@mail.com"]}
108
108
  ```
109
+
110
+ # Rails and ActionMailbox support
111
+
112
+ This gem also provides an Action Mailbox ingress, so you can receive inbound emails through Resend.
113
+
114
+ Configure Action Mailbox to use the Resend ingress, and make sure your API key is set (it is used to fetch the full message and its attachments):
115
+
116
+ ```ruby
117
+ # config/environments/production.rb
118
+ config.action_mailbox.ingress = :resend
119
+ ```
120
+
121
+ ```ruby
122
+ # config/initializers/resend.rb
123
+ Resend.api_key = "re_123456"
124
+ ```
125
+
126
+ Resend delivers inbound emails as `email.received` webhooks. Create a webhook in the [Resend dashboard](https://resend.com/webhooks) (or via `Resend::Webhooks.create`) subscribed to the `email.received` event and pointed at your app:
127
+
128
+ ```
129
+ https://example.com/rails/action_mailbox/resend/inbound_emails
130
+ ```
131
+
132
+ Then store the webhook's signing secret (`whsec_...`) so the ingress can verify incoming requests. Either add it to your encrypted credentials with `bin/rails credentials:edit`:
133
+
134
+ ```yml
135
+ action_mailbox:
136
+ resend_signing_secret: whsec_...
137
+ ```
138
+
139
+ or provide it through the `RESEND_INGRESS_SIGNING_SECRET` environment variable.
140
+
141
+ Each verified webhook is turned back into a full email — using Resend's raw message download when available, and otherwise rebuilding the message from the Received Emails and Attachments APIs — and handed to Action Mailbox for routing like any other ingress:
142
+
143
+ ```ruby
144
+ # app/mailboxes/application_mailbox.rb
145
+ class ApplicationMailbox < ActionMailbox::Base
146
+ routing /^support@/i => :support
147
+ end
148
+ ```
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionMailbox
4
+ module Ingresses
5
+ module Resend
6
+ # Ingests inbound emails delivered by Resend's +email.received+ webhook.
7
+ #
8
+ # Resend webhooks only carry the received email's metadata, so the full message
9
+ # (including any attachments) is fetched from the Resend API before being enqueued
10
+ # for routing. The webhook request is authenticated by verifying its Svix signature
11
+ # against the webhook's signing secret, which is stored in the
12
+ # +action_mailbox.resend_signing_secret+ Rails credential or the
13
+ # +RESEND_INGRESS_SIGNING_SECRET+ environment variable.
14
+ #
15
+ # Returns:
16
+ #
17
+ # - <tt>204 No Content</tt> if an inbound email is successfully recorded and enqueued for
18
+ # routing, or if the event is not an +email.received+ event and was ignored
19
+ # - <tt>401 Unauthorized</tt> if the request's signature could not be validated
20
+ # - <tt>404 Not Found</tt> if Action Mailbox is not configured to accept inbound emails
21
+ # from Resend
22
+ # - <tt>422 Unprocessable Entity</tt> if the request's payload is malformed
23
+ # - <tt>500 Server Error</tt> if the webhook signing secret is missing, the Resend API
24
+ # could not be reached, or one of the Active Record database, the Active Storage
25
+ # service, or the Active Job backend is misconfigured or unavailable
26
+ class InboundEmailsController < ActionMailbox::BaseController
27
+ before_action :authenticate
28
+
29
+ def create
30
+ return head :no_content unless email_received_event?
31
+ return head :unprocessable_entity if email_id.nil?
32
+
33
+ ActionMailbox::InboundEmail.create_and_extract_message_id! raw_email
34
+ head :no_content
35
+ rescue JSON::ParserError => e
36
+ logger.error e.message
37
+ head :unprocessable_entity
38
+ end
39
+
40
+ private
41
+
42
+ def raw_email
43
+ ::Resend::ActionMailbox::MessageBuilder.new(email_id).raw_email
44
+ end
45
+
46
+ def email_received_event?
47
+ event["type"] == "email.received"
48
+ end
49
+
50
+ def email_id
51
+ event.dig("data", "email_id")
52
+ end
53
+
54
+ def event
55
+ @event ||= JSON.parse(request.raw_post)
56
+ end
57
+
58
+ def authenticate
59
+ head :unauthorized unless authenticated?
60
+ end
61
+
62
+ def authenticated?
63
+ if signing_secret.present?
64
+ verified_signature?
65
+ else
66
+ raise ArgumentError, <<~MESSAGE.squish
67
+ Missing required Resend webhook signing secret. Set action_mailbox.resend_signing_secret
68
+ in your application's encrypted credentials or provide the RESEND_INGRESS_SIGNING_SECRET
69
+ environment variable.
70
+ MESSAGE
71
+ end
72
+ end
73
+
74
+ def verified_signature?
75
+ ::Resend::Webhooks.verify(
76
+ payload: request.raw_post,
77
+ headers: {
78
+ svix_id: request.headers["svix-id"],
79
+ svix_timestamp: request.headers["svix-timestamp"],
80
+ svix_signature: request.headers["svix-signature"]
81
+ },
82
+ webhook_secret: signing_secret
83
+ )
84
+ rescue RuntimeError
85
+ false
86
+ end
87
+
88
+ def signing_secret
89
+ Rails.application.credentials.dig(:action_mailbox, :resend_signing_secret) ||
90
+ ENV["RESEND_INGRESS_SIGNING_SECRET"]
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+
5
+ module Resend
6
+ module ActionMailbox
7
+ # Rails engine that routes Resend's +email.received+ webhooks to the
8
+ # Action Mailbox ingress controller.
9
+ class Engine < ::Rails::Engine
10
+ initializer "resend.action_mailbox.routes" do |app|
11
+ app.routes.append do
12
+ post "/rails/action_mailbox/resend/inbound_emails",
13
+ to: "action_mailbox/ingresses/resend/inbound_emails#create",
14
+ as: :rails_resend_inbound_emails
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mail"
4
+
5
+ module Resend
6
+ module ActionMailbox
7
+ # Rebuilds the full RFC 822 source of a received email so it can be ingested
8
+ # by Action Mailbox.
9
+ #
10
+ # Resend's +email.received+ webhook only carries metadata, so the message is
11
+ # fetched from the Received Emails API. When the API exposes a raw message
12
+ # download it is used as-is; otherwise the message is reconstructed as a
13
+ # Mail::Message from its parts, downloading each attachment through the
14
+ # attachments API.
15
+ class MessageBuilder
16
+ # Headers that describe the reconstructed MIME structure and therefore
17
+ # cannot be copied verbatim from the original message.
18
+ STRUCTURAL_HEADERS = %w[content-type content-transfer-encoding mime-version].freeze
19
+
20
+ # Envelope fields that are set from the API response when the original
21
+ # header of the same name is not available.
22
+ ENVELOPE_FIELDS = %i[from to cc bcc reply_to subject message_id].freeze
23
+
24
+ # @param email_id [String] The ID of the received email
25
+ def initialize(email_id)
26
+ @email_id = email_id
27
+ end
28
+
29
+ # @return [String] the full RFC 822 source of the received email
30
+ def raw_email
31
+ raw_download || rebuild_message.to_s
32
+ end
33
+
34
+ private
35
+
36
+ def email
37
+ @email ||= Resend::Emails::Receiving.get(@email_id, html_format: "cid")
38
+ end
39
+
40
+ def raw_download
41
+ url = fetch(email[:raw], "download_url") unless blank?(email[:raw])
42
+ download(url) unless blank?(url)
43
+ end
44
+
45
+ def rebuild_message
46
+ Mail.new.tap do |mail|
47
+ copy_headers(mail)
48
+ copy_envelope(mail)
49
+ add_bodies(mail)
50
+ add_attachments(mail)
51
+ end
52
+ end
53
+
54
+ def copy_headers(mail)
55
+ (email[:headers] || {}).each do |name, value|
56
+ next if STRUCTURAL_HEADERS.include?(name.to_s.downcase)
57
+
58
+ Array(value).each { |val| mail.header[name.to_s] = val }
59
+ end
60
+ end
61
+
62
+ def copy_envelope(mail)
63
+ ENVELOPE_FIELDS.each do |field|
64
+ value = email[field]
65
+ next if blank?(value) || mail.header[field.to_s.tr("_", "-")]
66
+
67
+ mail.public_send("#{field}=", value)
68
+ end
69
+
70
+ mail.date ||= email[:created_at]
71
+
72
+ expose_bcc(mail)
73
+ copy_received_for(mail)
74
+ end
75
+
76
+ # Mail omits Bcc when serializing a message by default, but Action Mailbox
77
+ # needs it to route the email.
78
+ def expose_bcc(mail)
79
+ bcc = mail.header["bcc"]
80
+ bcc.include_in_headers = true if bcc.respond_to?(:include_in_headers)
81
+ end
82
+
83
+ # Preserve the addresses this email was received for (e.g. through a
84
+ # forwarding rule) so Action Mailbox can route on them.
85
+ def copy_received_for(mail)
86
+ Array(email[:received_for]).each { |address| mail.header["X-Original-To"] = address }
87
+ end
88
+
89
+ def add_bodies(mail)
90
+ html = presence(email[:html])
91
+ text = presence(email[:text])
92
+
93
+ if html
94
+ mail.text_part = build_part("text/plain", text) if text
95
+ mail.html_part = build_part("text/html", html)
96
+ elsif text
97
+ mail.content_type "text/plain; charset=UTF-8"
98
+ mail.body text
99
+ end
100
+ end
101
+
102
+ def build_part(mime_type, content)
103
+ Mail::Part.new(content_type: "#{mime_type}; charset=UTF-8", body: content)
104
+ end
105
+
106
+ def add_attachments(mail)
107
+ Array(email[:attachments]).each { |meta| add_attachment(mail, meta) }
108
+ end
109
+
110
+ def add_attachment(mail, meta)
111
+ details = Resend::Emails::Receiving::Attachments.get(email_id: @email_id, id: fetch(meta, "id"))
112
+ filename = fetch(meta, "filename") || details[:filename]
113
+
114
+ mail.attachments[filename] = {
115
+ mime_type: fetch(meta, "content_type") || details[:content_type],
116
+ content: download(details[:download_url])
117
+ }
118
+
119
+ decorate_attachment(mail.attachments.last, meta)
120
+ end
121
+
122
+ def decorate_attachment(part, meta)
123
+ content_id = fetch(meta, "content_id")
124
+ part.content_id = normalize_content_id(content_id) unless blank?(content_id)
125
+
126
+ return unless fetch(meta, "content_disposition") == "inline"
127
+
128
+ part.content_disposition = "inline; filename=\"#{part.filename}\""
129
+ end
130
+
131
+ def normalize_content_id(content_id)
132
+ content_id.start_with?("<") ? content_id : "<#{content_id}>"
133
+ end
134
+
135
+ def download(url)
136
+ response = HTTParty.get(url)
137
+
138
+ unless response.code == 200
139
+ raise Resend::Error::ServerError.new(
140
+ "Failed to download received email content (HTTP #{response.code})", response.code
141
+ )
142
+ end
143
+
144
+ response.body
145
+ end
146
+
147
+ # Nested objects come back from the API with string keys, but hand-built
148
+ # hashes (tests, console usage) often use symbols. Accept both.
149
+ def fetch(hash, key)
150
+ hash[key] || hash[key.to_sym]
151
+ end
152
+
153
+ def presence(value)
154
+ value unless blank?(value)
155
+ end
156
+
157
+ def blank?(value)
158
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
159
+ end
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resend"
4
+ require "resend/action_mailbox/message_builder"
5
+ require "resend/action_mailbox/engine" if defined?(::Rails::Engine)
@@ -58,14 +58,22 @@ module Resend
58
58
  # @example List contacts with pagination
59
59
  # Resend::Contacts.list(limit: 10)
60
60
  #
61
+ # @example List contacts scoped to a segment
62
+ # Resend::Contacts.list(segment_id: "seg_456", limit: 10)
63
+ #
61
64
  # @example List contacts scoped to an audience
62
65
  # Resend::Contacts.list(audience_id: "aud_456", limit: 10)
63
66
  #
64
67
  # https://resend.com/docs/api-reference/contacts/list-contacts
65
68
  def list(params = {})
66
- audience_id = params[:audience_id]
67
- path = if audience_id
68
- Resend::PaginationHelper.build_paginated_path("audiences/#{audience_id}/contacts", params)
69
+ path = if params[:audience_id]
70
+ audience_id = params[:audience_id]
71
+ Resend::PaginationHelper.build_paginated_path("audiences/#{audience_id}/contacts",
72
+ params.except(:audience_id))
73
+ elsif params[:segment_id]
74
+ segment_id = params[:segment_id]
75
+ Resend::PaginationHelper.build_paginated_path("segments/#{segment_id}/contacts",
76
+ params.except(:segment_id))
69
77
  else
70
78
  Resend::PaginationHelper.build_paginated_path("contacts", params)
71
79
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Resend
4
- VERSION = "1.13.0"
4
+ VERSION = "1.14.0"
5
5
  end
@@ -117,6 +117,22 @@ module Resend
117
117
  Resend::Request.new(path, {}, "get").perform
118
118
  end
119
119
 
120
+ # Replay a webhook event
121
+ #
122
+ # Queues one more delivery of the event to the webhook. Manual replays do not schedule automatic retries.
123
+ #
124
+ # @param webhook_id [String] The webhook ID
125
+ # @param event_id [String] The webhook event ID
126
+ #
127
+ # @return [Hash] The replayed webhook event id and object type
128
+ #
129
+ # @example
130
+ # Resend::Webhooks.replay_event("4dd369bc-aa82-4ff3-97de-514ae3000ee0", "msg_123")
131
+ def replay_event(webhook_id, event_id)
132
+ path = "webhooks/#{webhook_id}/events/#{event_id}/replay"
133
+ Resend::Request.new(path, {}, "post").perform
134
+ end
135
+
120
136
  # Retrieve delivery attempts for a webhook event
121
137
  #
122
138
  # @param webhook_id [String] The webhook ID
data/lib/resend.rb CHANGED
@@ -44,6 +44,7 @@ require "resend/suppressions/batch"
44
44
 
45
45
  # Rails
46
46
  require "resend/railtie" if defined?(Rails) && defined?(ActionMailer)
47
+ require "resend/action_mailbox" if defined?(Rails) && defined?(ActionMailbox)
47
48
 
48
49
  # Main Resend module
49
50
  module Resend
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: resend
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.0
4
+ version: 1.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derich Pacheco
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-08-26 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: base64
@@ -38,7 +37,6 @@ dependencies:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: 0.22.0
41
- description:
42
40
  email: carlosderich@gmail.com
43
41
  executables: []
44
42
  extensions: []
@@ -46,7 +44,11 @@ extra_rdoc_files: []
46
44
  files:
47
45
  - CHANGELOG.md
48
46
  - README.md
47
+ - app/controllers/action_mailbox/ingresses/resend/inbound_emails_controller.rb
49
48
  - lib/resend.rb
49
+ - lib/resend/action_mailbox.rb
50
+ - lib/resend/action_mailbox/engine.rb
51
+ - lib/resend/action_mailbox/message_builder.rb
50
52
  - lib/resend/api_keys.rb
51
53
  - lib/resend/automations.rb
52
54
  - lib/resend/automations/runs.rb
@@ -85,7 +87,6 @@ homepage: https://github.com/resend/resend-ruby
85
87
  licenses:
86
88
  - MIT
87
89
  metadata: {}
88
- post_install_message:
89
90
  rdoc_options: []
90
91
  require_paths:
91
92
  - lib
@@ -100,8 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
101
  - !ruby/object:Gem::Version
101
102
  version: '0'
102
103
  requirements: []
103
- rubygems_version: 3.0.3.1
104
- signing_key:
104
+ rubygems_version: 4.0.16
105
105
  specification_version: 4
106
106
  summary: The Ruby and Rails SDK for resend.com
107
107
  test_files: []