From cd9047621544a351aef99a7abcfe445ca8d46cc4 Mon Sep 17 00:00:00 2001 From: Andrew Timberlake Date: Thu, 4 Dec 2025 15:51:37 +0200 Subject: [PATCH] Only encode headers with non-ASCII characters --- lib/mail/renderers/rfc_2822.ex | 15 ++++++++++++--- test/mail/renderers/rfc_2822_test.exs | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/lib/mail/renderers/rfc_2822.ex b/lib/mail/renderers/rfc_2822.ex index 53187cf6..1ffccb44 100644 --- a/lib/mail/renderers/rfc_2822.ex +++ b/lib/mail/renderers/rfc_2822.ex @@ -207,13 +207,22 @@ defmodule Mail.Renderers.RFC2822 do # As stated at https://datatracker.ietf.org/doc/html/rfc2047#section-2, encoded words must be # split in 76 chars including its surroundings and delimmiters. # Since enclosing starts with =?UTF-8?Q? and ends with ?=, max length should be 64 + # Per RFC 2047, encoding is only required for non-ASCII characters. + # ASCII-only headers should not be encoded, regardless of length. + # Per RFC 2047, ASCII-only headers should not be encoded, regardless of length defp encode_header_value(header_value, :quoted_printable) do - case Mail.Encoders.QuotedPrintable.encode(header_value, 64) do - ^header_value -> header_value - encoded -> wrap_encoded_words(encoded) + if contains_non_ascii?(header_value) do + header_value |> Mail.Encoders.QuotedPrintable.encode(64) |> wrap_encoded_words() + else + header_value end end + # Check if a string contains any non-ASCII characters (bytes > 0x7F) + defp contains_non_ascii?(<<>>), do: false + defp contains_non_ascii?(<>) when byte > 127, do: true + defp contains_non_ascii?(<<_byte, rest::binary>>), do: contains_non_ascii?(rest) + defp wrap_encoded_words(value) do :binary.split(value, "=\r\n", [:global]) |> Enum.map(fn chunk -> <<"=?UTF-8?Q?", chunk::binary, "?=">> end) diff --git a/test/mail/renderers/rfc_2822_test.exs b/test/mail/renderers/rfc_2822_test.exs index a73d0105..2d490e1a 100644 --- a/test/mail/renderers/rfc_2822_test.exs +++ b/test/mail/renderers/rfc_2822_test.exs @@ -41,6 +41,28 @@ defmodule Mail.Renderers.RFC2822Test do assert header == "Content-Disposition: attachment; filename=\"my;test;file\"" end + test "encodes header if necessary" do + assert Mail.Renderers.RFC2822.render_header("Subject", [ + "Hello World!" + ]) == "Subject: Hello World!" + + assert Mail.Renderers.RFC2822.render_header("Subject", [ + String.duplicate("a", 73) + ]) == "Subject: #{String.duplicate("a", 73)}" + + assert Mail.Renderers.RFC2822.render_header("Subject", [ + "Hello World 😀" + ]) == "Subject: =?UTF-8?Q?Hello World =F0=9F=98=80?=" + + assert Mail.Renderers.RFC2822.render_header("Subject", [ + "Café résumé" + ]) == "Subject: =?UTF-8?Q?Caf=C3=A9 r=C3=A9sum=C3=A9?=" + + assert Mail.Renderers.RFC2822.render_header("Subject", [ + "Hello 世界 World" + ]) == "Subject: =?UTF-8?Q?Hello =E4=B8=96=E7=95=8C World?=" + end + test "address headers renders list of recipients" do header = Mail.Renderers.RFC2822.render_header("from", "user1@example.com") assert header == "From: user1@example.com"