From 9e362b0627efc78cececd899b1a365ba584586d7 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Mon, 10 Aug 2026 01:06:12 -0400 Subject: [PATCH] Fix NameError in Mailgun and Mailjet on Ruby 3.4 Both providers called `Base64.strict_encode64` in `default_headers` without requiring `base64`. Ruby 3.4 moved `base64` out of the default gems, so the constant is undefined and every delivery raises NameError. Since the gemspec already requires Ruby >= 3.4, this affects every supported version. Encode with `Array#pack("m0")` instead of requiring `base64`. It is what `strict_encode64` uses internally and produces identical output, so the fix adds no new dependency to the gemspec. Adds test coverage for both providers, including the authentication headers that reproduce the bug. Fixes #56 --- lib/courrier/email/providers/mailgun.rb | 2 +- lib/courrier/email/providers/mailjet.rb | 2 +- test/courrier/email/providers/mailgun_test.rb | 86 +++++++++++++++++++ test/courrier/email/providers/mailjet_test.rb | 70 +++++++++++++++ 4 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 test/courrier/email/providers/mailgun_test.rb create mode 100644 test/courrier/email/providers/mailjet_test.rb diff --git a/lib/courrier/email/providers/mailgun.rb b/lib/courrier/email/providers/mailgun.rb index 7afb3ca..a2f74f3 100644 --- a/lib/courrier/email/providers/mailgun.rb +++ b/lib/courrier/email/providers/mailgun.rb @@ -33,7 +33,7 @@ def content_type = "multipart/form-data" def default_headers { - "Authorization" => "Basic #{Base64.strict_encode64("api:#{@api_key}")}" + "Authorization" => "Basic #{["api:#{@api_key}"].pack("m0")}" } end end diff --git a/lib/courrier/email/providers/mailjet.rb b/lib/courrier/email/providers/mailjet.rb index 2ffeb44..fe43574 100644 --- a/lib/courrier/email/providers/mailjet.rb +++ b/lib/courrier/email/providers/mailjet.rb @@ -35,7 +35,7 @@ def body def default_headers { - "Authorization" => "Basic " + Base64.strict_encode64("#{@api_key}:#{@provider_options.api_secret}") + "Authorization" => "Basic #{["#{@api_key}:#{@provider_options.api_secret}"].pack("m0")}" } end diff --git a/test/courrier/email/providers/mailgun_test.rb b/test/courrier/email/providers/mailgun_test.rb new file mode 100644 index 0000000..7374313 --- /dev/null +++ b/test/courrier/email/providers/mailgun_test.rb @@ -0,0 +1,86 @@ +require "test_helper" + +module Courrier::Email::Providers + class MailgunTest < Minitest::Test + def setup + email = TestEmail.new( + from: "devs@railsdesigner.com", + to: "first@example.com, second@example.com", + reply_to: "support@railsdesigner.com" + ) + + @provider = provider_for(email) + end + + def test_formats_transactional_email + assert_equal( + { + "from" => "devs@railsdesigner.com", + "to" => "first@example.com, second@example.com", + "h:Reply-To" => "support@railsdesigner.com", + "subject" => "Test Subject", + "text" => "Test Body", + "html" => "

Test HTML Body

" + }, + @provider.body + ) + end + + def test_omits_reply_to_when_not_set + body = provider_for(TestEmail.new(from: "devs@railsdesigner.com", to: "first@example.com")).body + + refute_includes body.keys, "h:Reply-To" + end + + def test_builds_endpoint_url_from_domain + assert_equal "https://api.mailgun.net/v3/railsdesigner.com/messages", @provider.send(:endpoint_url) + end + + def test_requires_a_domain + provider = Mailgun.new( + api_key: "test_key", + options: TestEmail.new(from: "devs@railsdesigner.com", to: "first@example.com").options, + provider_options: Courrier::Configuration::ProviderConfig.new + ) + + error = assert_raises(Courrier::ArgumentError) { provider.send(:endpoint_url) } + + assert_match(/domain/, error.message) + end + + def test_authenticates_with_api_key + assert_equal( + {"Authorization" => "Basic YXBpOnRlc3Rfa2V5"}, + @provider.send(:default_headers) + ) + end + + def test_submits_as_multipart_form_data + assert_equal "multipart/form-data", @provider.send(:content_type) + end + + def test_is_available_through_provider_registry + mock_provider = Minitest::Mock.new + mock_provider.expect(:deliver, nil) + + Mailgun.stub :new, mock_provider do + Courrier::Email::Provider.new( + provider: "mailgun", + api_key: "test_key", + options: @provider.instance_variable_get(:@options) + ).deliver + end + + mock_provider.verify + end + + private + + def provider_for(email) + provider_options = Courrier::Configuration::ProviderConfig.new + provider_options.domain = "railsdesigner.com" + + Mailgun.new(api_key: "test_key", options: email.options, provider_options: provider_options) + end + end +end diff --git a/test/courrier/email/providers/mailjet_test.rb b/test/courrier/email/providers/mailjet_test.rb new file mode 100644 index 0000000..d02f052 --- /dev/null +++ b/test/courrier/email/providers/mailjet_test.rb @@ -0,0 +1,70 @@ +require "test_helper" + +module Courrier::Email::Providers + class MailjetTest < Minitest::Test + def setup + email = TestEmail.new( + from: "devs@railsdesigner.com", + to: "first@example.com", + reply_to: "support@railsdesigner.com" + ) + + @provider = provider_for(email) + end + + def test_formats_transactional_email + assert_equal( + { + "Messages" => [ + { + "From" => {"Email" => "devs@railsdesigner.com"}, + "To" => [{"Email" => "first@example.com"}], + "ReplyTo" => {"Email" => "support@railsdesigner.com"}, + "Subject" => "Test Subject", + "TextPart" => "Test Body", + "HTMLPart" => "

Test HTML Body

" + } + ] + }, + @provider.body + ) + end + + def test_omits_reply_to_when_not_set + body = provider_for(TestEmail.new(from: "devs@railsdesigner.com", to: "first@example.com")).body + + refute_includes body["Messages"].first.keys, "ReplyTo" + end + + def test_authenticates_with_api_key_and_secret + assert_equal( + {"Authorization" => "Basic dGVzdF9rZXk6dGVzdF9zZWNyZXQ="}, + @provider.send(:default_headers) + ) + end + + def test_is_available_through_provider_registry + mock_provider = Minitest::Mock.new + mock_provider.expect(:deliver, nil) + + Mailjet.stub :new, mock_provider do + Courrier::Email::Provider.new( + provider: "mailjet", + api_key: "test_key", + options: @provider.instance_variable_get(:@options) + ).deliver + end + + mock_provider.verify + end + + private + + def provider_for(email) + provider_options = Courrier::Configuration::ProviderConfig.new + provider_options.api_secret = "test_secret" + + Mailjet.new(api_key: "test_key", options: email.options, provider_options: provider_options) + end + end +end