diff --git a/.env.example b/.env.example index a8fdab64a..cd20ca72a 100644 --- a/.env.example +++ b/.env.example @@ -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= diff --git a/Gemfile b/Gemfile index 92c2d8e72..769022c1d 100644 --- a/Gemfile +++ b/Gemfile @@ -178,6 +178,8 @@ gem 'ruby-openai' gem 'shopify_api' +gem 'resend', '~> 0.19.0' + ### Gems required only in specific deployment environments ### ############################################################## diff --git a/Gemfile.lock b/Gemfile.lock index ebaa749dd..637f5f79d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb index 9405c99b1..61c48c68d 100644 --- a/app/views/devise/mailer/reset_password_instructions.html.erb +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -4,5 +4,7 @@

<%= link_to 'Change my password', frontend_url('auth/password/edit', reset_password_token: @token) %>

+

Copy and paste the URL into your browser if the link above doesn't work: <%= frontend_url('auth/password/edit', reset_password_token: @token) %>

+

If you didn't request this, please ignore this email.

Your password won't change until you access the link above and create a new one.

diff --git a/config/initializers/mailer.rb b/config/initializers/mailer.rb index a0e5d7b73..d1093a108 100644 --- a/config/initializers/mailer.rb +++ b/config/initializers/mailer.rb @@ -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 ######################################### diff --git a/config/initializers/resend.rb b/config/initializers/resend.rb new file mode 100644 index 000000000..e28d7e527 --- /dev/null +++ b/config/initializers/resend.rb @@ -0,0 +1,3 @@ +require 'resend' + +Resend.api_key = ENV.fetch('RESEND_API_KEY', nil) diff --git a/lib/mail/resend_provider.rb b/lib/mail/resend_provider.rb new file mode 100644 index 000000000..f151abfcf --- /dev/null +++ b/lib/mail/resend_provider.rb @@ -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 diff --git a/spec/lib/mail/resend_provider_spec.rb b/spec/lib/mail/resend_provider_spec.rb new file mode 100644 index 000000000..fc4d126b0 --- /dev/null +++ b/spec/lib/mail/resend_provider_spec.rb @@ -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: '

This is a test email message.

') + 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: '

This is a test email message.

', + 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: '

This is a test email message.

', + 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