diff --git a/lib/courrier/email/options.rb b/lib/courrier/email/options.rb index 1506b6b..b5fb50f 100644 --- a/lib/courrier/email/options.rb +++ b/lib/courrier/email/options.rb @@ -56,7 +56,10 @@ def wrap(content, with_layout:) next wrapped if !layout - layout % {content: wrapped} + # A plain substitution, not `String#%`: an HTML layout routinely carries a + # bare `%` (`width: 100%`, an encoded URL), and `format` raises on those. + # The block form also keeps the content verbatim when it contains `\1`, `\\`, etc. + layout.gsub("%{content}") { wrapped } end end diff --git a/test/courrier/email/options_test.rb b/test/courrier/email/options_test.rb new file mode 100644 index 0000000..b8dbbcd --- /dev/null +++ b/test/courrier/email/options_test.rb @@ -0,0 +1,44 @@ +require "test_helper" + +class Courrier::Email::OptionsTest < Minitest::Test + def test_layout_wraps_the_content_at_the_content_token + options = build_options( + html: "

Hi

", + text: "Hi", + layouts: [{html: "
%{content}
", text: "%{content}\n\nThanks!"}] + ) + + assert_equal "

Hi

", options.html + assert_equal "Hi\n\nThanks!", options.text + end + + # An HTML email layout almost always carries a bare `%` (`width: 100%`, an + # encoded URL). `String#%` treats it as a format directive and raises. + def test_layout_keeps_a_literal_percent_sign + options = build_options( + html: "

Hi

", + layouts: [{html: "%{content}"}] + ) + + assert_equal "

Hi

", options.html + end + + # gsub's string replacement would eat `\1`, `\\`, `\&` in the content; the + # wrapped body has to come through byte for byte. + def test_layout_keeps_backslash_sequences_in_the_content + options = build_options( + text: 'refund code \1 (\\ and \& too)', + layouts: [{text: "%{content}\n--"}] + ) + + assert_equal "refund code \\1 (\\ and \\& too)\n--", options.text + end + + private + + def build_options(**overrides) + Courrier::Email::Options.new( + {from: "devs@railsdesigner.com", to: "recipient@railsdesigner.com"}.merge(overrides) + ) + end +end