feat: add Resend email delivery method and configuration (#11)

* feat: add Resend email delivery method and configuration

* chore: simplify ResendProvider initialization by removing settings parameter

* test: update ResendProvider initialization in tests by removing unnecessary settings parameter

* chore: improvements
This commit is contained in:
Gabriel Jablonski 2025-04-01 23:29:18 -03:00 committed by gabrieljablonski
parent a9f0dc35f9
commit 5b7fc9ae77
8 changed files with 103 additions and 0 deletions

View File

@ -262,3 +262,5 @@ AZURE_APP_SECRET=
BAILEYS_PROVIDER_DEFAULT_CLIENT_NAME=Chatwoot
BAILEYS_PROVIDER_DEFAULT_URL=http://localhost:3025
BAILEYS_PROVIDER_DEFAULT_API_KEY=
RESEND_API_KEY=

View File

@ -178,6 +178,8 @@ gem 'ruby-openai'
gem 'shopify_api'
gem 'resend', '~> 0.19.0'
### Gems required only in specific deployment environments ###
##############################################################

View File

@ -634,6 +634,8 @@ GEM
uber (< 0.2.0)
request_store (1.5.1)
rack (>= 1.4)
resend (0.19.0)
httparty (>= 0.21.0)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
@ -957,6 +959,7 @@ DEPENDENCIES
rails (~> 7.0.8.4)
redis
redis-namespace
resend (~> 0.19.0)
responders (>= 3.1.1)
rest-client
reverse_markdown

View File

@ -4,5 +4,7 @@
<p><%= link_to 'Change my password', frontend_url('auth/password/edit', reset_password_token: @token) %></p>
<p style="color: #999999; font-size: 12px;">Copy and paste the URL into your browser if the link above doesn't work: <%= frontend_url('auth/password/edit', reset_password_token: @token) %></p>
<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>

View File

@ -1,3 +1,5 @@
require_relative '../../lib/mail/resend_provider'
Rails.application.configure do
#########################################
# Configuration Related to Action Mailer
@ -36,6 +38,9 @@ Rails.application.configure do
# You can use letter opener for your local development by setting the environment variable
config.action_mailer.delivery_method = :letter_opener if Rails.env.development? && ENV['LETTER_OPENER']
config.action_mailer.delivery_method = :resend if ENV['RESEND_API_KEY'].present?
ActionMailer::Base.add_delivery_method :resend, Mail::ResendProvider
#########################################
# Configuration Related to Action MailBox
#########################################

View File

@ -0,0 +1,3 @@
require 'resend'
Resend.api_key = ENV.fetch('RESEND_API_KEY', nil)

View File

@ -0,0 +1,29 @@
module Mail # rubocop:disable Style/ClassAndModuleChildren
class ResendProvider
class DeliveryError < StandardError; end
def initialize(_settings); end # rubocop:disable Style/RedundantInitialize
def deliver!(mail)
Resend::Emails.send(
from: mail.smtp_envelope_from,
to: mail.smtp_envelope_to,
subject: mail.subject,
html: mail.decoded,
text: sanitize_html(mail.decoded)
)
rescue Resend::Error => e
raise DeliveryError, "Failed to send email: #{e.message}"
rescue StandardError => e
raise DeliveryError, "An error occurred while sending email: #{e.message}"
end
private
def sanitize_html(html)
sanitized = ActionView::Base.full_sanitizer.sanitize(html)
# NOTE: Remove more than two consecutive newlines
sanitized.lines.map(&:strip).join("\n").gsub(/\n{3,}/, "\n\n").strip
end
end
end

View File

@ -0,0 +1,57 @@
require 'rails_helper'
describe Mail::ResendProvider do
let(:provider) { described_class.new({}) }
let(:mail) do
instance_double(Mail::Message,
smtp_envelope_from: 'sender@example.com',
smtp_envelope_to: ['receiver@example.com'],
subject: 'Test Email',
decoded: '<p>This is a test email message.</p>')
end
describe '#deliver!' do
it 'calls Resend with the correct parameters' do
response = instance_double(HTTParty::Response, success?: true)
allow(Resend::Emails).to receive(:send)
.with(
from: 'sender@example.com',
to: ['receiver@example.com'],
subject: 'Test Email',
html: '<p>This is a test email message.</p>',
text: 'This is a test email message.'
)
.and_return(response)
provider.deliver!(mail)
expect(Resend::Emails).to have_received(:send)
end
context 'when response is not successful' do
it 'raises a DeliveryError with the error message' do
allow(Resend::Emails).to receive(:send)
.with(
from: 'sender@example.com',
to: ['receiver@example.com'],
subject: 'Test Email',
html: '<p>This is a test email message.</p>',
text: 'This is a test email message.'
)
.and_raise(Resend::Error.new('Service unavailable'))
expect { provider.deliver!(mail) }
.to raise_error(described_class::DeliveryError, 'Failed to send email: Service unavailable')
end
end
context 'when an exception occurs during sending' do
it 'raises a DeliveryError with the exception message' do
allow(Resend::Emails).to receive(:send).and_raise(StandardError, 'Connection timed out')
expect { provider.deliver!(mail) }
.to raise_error(described_class::DeliveryError, 'An error occurred while sending email: Connection timed out')
end
end
end
end