From 3414919e32b4011a9673c568d98250967595cfa6 Mon Sep 17 00:00:00 2001 From: Jan Schlosser Date: Tue, 20 Oct 2026 09:41:11 +0200 Subject: [PATCH] only ignore whitespace between adjacent encoded words MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The whitespace stripping added in #168 was unanchored and removed whitespace before every `=?` in the remainder, so plain text followed by an encoded word lost its separating space (e.g. "a x =?..?=" became "a xb"). Per RFC 2047 §6.2, only linear white space between two adjacent encoded words is ignored. Co-Authored-By: Claude Opus 5.5 --- lib/mail/parsers/rfc_2822.ex | 3 +- test/mail/parsers/rfc_2822_test.exs | 49 +++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/lib/mail/parsers/rfc_2822.ex b/lib/mail/parsers/rfc_2822.ex index e4d7df72..b470928b 100644 --- a/lib/mail/parsers/rfc_2822.ex +++ b/lib/mail/parsers/rfc_2822.ex @@ -540,7 +540,8 @@ defmodule Mail.Parsers.RFC2822 do decoded_string = charset_handler.(charset, decoded_string) # Remove space if immediately followed by another encoded word string - remainder = Regex.replace(~r/\s+\=\?/, remainder, "=?") + remainder = + Regex.replace(~r/\A(?:(?:\r\n)?[ \t])+(?==\?[^?]+\?[BbQq]\?[^?]*\?=)/, remainder, "") decoded_string <> parse_encoded_word(remainder, opts) diff --git a/test/mail/parsers/rfc_2822_test.exs b/test/mail/parsers/rfc_2822_test.exs index 515cad30..e3040206 100644 --- a/test/mail/parsers/rfc_2822_test.exs +++ b/test/mail/parsers/rfc_2822_test.exs @@ -546,6 +546,55 @@ defmodule Mail.Parsers.RFC2822Test do assert message.headers["x-encoded-word"] == "hεłłø wørłd" end + test "ignores whitespace between adjacent encoded words (RFC 2047 §6.2)" do + message = + parse_email(""" + X-Adjacent: =?utf-8?Q?a?= =?utf-8?Q?b?= + X-Folded-Space: =?utf-8?Q?a?= + =?utf-8?Q?b?= + X-Folded-Tab: =?utf-8?Q?a?= + \t=?utf-8?Q?b?= + + Body + """) + + assert message.headers["x-adjacent"] == "ab" + assert message.headers["x-folded-space"] == "ab" + assert message.headers["x-folded-tab"] == "ab" + end + + test "only ignores linear white space between adjacent encoded words (RFC 2047 §6.2)" do + message = + Mail.Parsers.RFC2822.parse( + "X-LF: =?utf-8?Q?a?=\n=?utf-8?Q?b?=\r\n" <> + "X-CR: =?utf-8?Q?a?=\r=?utf-8?Q?b?=\r\n" <> + "X-VT: =?utf-8?Q?a?=\v=?utf-8?Q?b?=\r\n" <> + "X-FF: =?utf-8?Q?a?=\f=?utf-8?Q?b?=\r\n" <> + "\r\nBody\r\n" + ) + + assert message.headers["x-lf"] == "a\nb" + assert message.headers["x-cr"] == "a\rb" + assert message.headers["x-vt"] == "a\vb" + assert message.headers["x-ff"] == "a\fb" + end + + test "keeps whitespace between encoded word and plain text (RFC 2047 §6.2)" do + message = + parse_email(""" + X-Mixed: =?utf-8?Q?a?= x =?utf-8?Q?b?= + X-Stray: =?utf-8?Q?a?= =? x + Subject: =?utf-8?Q?Gr=C3=BC=C3=9Fe?= from the + =?utf-8?Q?M=C3=BCller?= family + + Body + """) + + assert message.headers["x-mixed"] == "a x b" + assert message.headers["x-stray"] == "a =? x" + assert message.headers["subject"] == "Grüße from the Müller family" + end + test "allow empty body (RFC2822 §3.5)" do message = parse_email("""