From 881d82c008af17da1cb9109387112eb3978250ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:51:23 +0000 Subject: [PATCH 1/6] Initial plan From d09f70e4bebf31bb3fedc4fbc04fc36da01a657e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:21:49 +0000 Subject: [PATCH 2/6] Reject/strip CR/LF in MailAddress.DisplayName to prevent corrupted SMTP headers Co-authored-by: MihaZupan <25307628+MihaZupan@users.noreply.github.com> --- .../src/System/Net/Mail/MailAddress.cs | 40 ++++++++++++++++++- .../MailAddressDisplayNameTest.cs | 16 ++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs index a4cc338453b8ed..ecd69967f0380e 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs @@ -131,9 +131,10 @@ private static bool TryParse([NotNullWhen(true)] string? address, string? displa displayNameEncoding ??= Encoding.GetEncoding(MimeBasePart.DefaultCharSet); displayName ??= string.Empty; + bool userProvidedDisplayName = !string.IsNullOrEmpty(displayName); // Check for bounding quotes - if (!string.IsNullOrEmpty(displayName)) + if (userProvidedDisplayName) { if (!MailAddressParser.TryNormalizeOrThrow(displayName, out displayName, throwExceptionIfFail)) { @@ -141,6 +142,20 @@ private static bool TryParse([NotNullWhen(true)] string? address, string? displa return false; } + // Reject CR/LF in caller-supplied display names. Such characters would corrupt + // the encoded mail header when the address is serialized into a message + // (e.g. From/To headers written by SmtpClient). + if (MailBnfHelper.HasCROrLF(displayName)) + { + if (throwExceptionIfFail) + { + throw new FormatException(SR.MailAddressInvalidFormat); + } + + parsedData = default; + return false; + } + if (displayName.Length >= 2 && displayName.StartsWith('\"') && displayName.EndsWith('\"')) { // Peel bounding quotes, they'll get re-added later. @@ -155,9 +170,17 @@ private static bool TryParse([NotNullWhen(true)] string? address, string? displa } // If we were not given a display name, use the one parsed from 'address'. - if (string.IsNullOrEmpty(displayName)) + if (!userProvidedDisplayName) { displayName = info.DisplayName; + + // The display name parsed out of 'address' may contain folding white space + // (CRLF inside a quoted string, RFC 5322 §3.2.4). Unfold by stripping any + // CR/LF characters so re-encoding into a header does not corrupt the output. + if (!string.IsNullOrEmpty(displayName) && MailBnfHelper.HasCROrLF(displayName)) + { + displayName = StripCRLF(displayName); + } } parsedData = (displayName, info.User, info.Host, displayNameEncoding); @@ -165,6 +188,19 @@ private static bool TryParse([NotNullWhen(true)] string? address, string? displa return true; } + private static string StripCRLF(string value) + { + ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[256]); + foreach (char c in value) + { + if (c != '\r' && c != '\n') + { + sb.Append(c); + } + } + return sb.ToString(); + } + public string DisplayName { get diff --git a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressDisplayNameTest.cs b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressDisplayNameTest.cs index 198ff9c24aad8a..54e40c1a06ad83 100644 --- a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressDisplayNameTest.cs +++ b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressDisplayNameTest.cs @@ -61,5 +61,21 @@ public void MailAddress_Ctor_FullString_Succeeds(string address, string displayN Assert.Equal(expectedDisplayName ?? displayName, mailAddress.DisplayName); Assert.Equal(expectedToString, mailAddress.ToString()); } + + [Theory] + [InlineData("Display\rName")] + [InlineData("Display\nName")] + [InlineData("Display\r\nName")] + [InlineData("DisplayName\r")] + [InlineData("DisplayName\n")] + [InlineData("DisplayName\r\n")] + [InlineData("\rDisplayName")] + [InlineData("\nDisplayName")] + [InlineData("\"Display\r\nName\"")] + public void MailAddress_Ctor_DisplayNameContainsCRLF_Throws(string displayName) + { + Assert.Throws(() => new MailAddress(Address, displayName)); + Assert.False(MailAddress.TryCreate(Address, displayName, out _)); + } } } From b0ae3eac30a4f0c473c26c1723daa349ab9162d4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:12:35 +0000 Subject: [PATCH 3/6] Address review: use inline IsNullOrEmpty checks instead of userProvidedDisplayName flag Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../System.Net.Mail/src/System/Net/Mail/MailAddress.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs index ecd69967f0380e..79ce805ff7095d 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs @@ -131,10 +131,9 @@ private static bool TryParse([NotNullWhen(true)] string? address, string? displa displayNameEncoding ??= Encoding.GetEncoding(MimeBasePart.DefaultCharSet); displayName ??= string.Empty; - bool userProvidedDisplayName = !string.IsNullOrEmpty(displayName); // Check for bounding quotes - if (userProvidedDisplayName) + if (!string.IsNullOrEmpty(displayName)) { if (!MailAddressParser.TryNormalizeOrThrow(displayName, out displayName, throwExceptionIfFail)) { @@ -170,7 +169,7 @@ private static bool TryParse([NotNullWhen(true)] string? address, string? displa } // If we were not given a display name, use the one parsed from 'address'. - if (!userProvidedDisplayName) + if (string.IsNullOrEmpty(displayName)) { displayName = info.DisplayName; From 492dd928878fb807743dac584135d1f8ec66bc2d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:22:14 +0000 Subject: [PATCH 4/6] Align MailAddress CRLF handling with parser validation Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../src/System/Net/Mail/MailAddress.cs | 21 ------------ .../src/System/Net/Mail/MailAddressParser.cs | 14 ++++++++ .../Functional/SmtpClientSendMailTest.cs | 8 ++--- .../MailAddressTests/MailAddressParserTest.cs | 34 +++++++++++++++++++ .../MailAddressParsingTest.cs | 11 +++--- 5 files changed, 57 insertions(+), 31 deletions(-) diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs index 79ce805ff7095d..1b26b83856921d 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs @@ -172,14 +172,6 @@ private static bool TryParse([NotNullWhen(true)] string? address, string? displa if (string.IsNullOrEmpty(displayName)) { displayName = info.DisplayName; - - // The display name parsed out of 'address' may contain folding white space - // (CRLF inside a quoted string, RFC 5322 §3.2.4). Unfold by stripping any - // CR/LF characters so re-encoding into a header does not corrupt the output. - if (!string.IsNullOrEmpty(displayName) && MailBnfHelper.HasCROrLF(displayName)) - { - displayName = StripCRLF(displayName); - } } parsedData = (displayName, info.User, info.Host, displayNameEncoding); @@ -187,19 +179,6 @@ private static bool TryParse([NotNullWhen(true)] string? address, string? displa return true; } - private static string StripCRLF(string value) - { - ValueStringBuilder sb = new ValueStringBuilder(stackalloc char[256]); - foreach (char c in value) - { - if (c != '\r' && c != '\n') - { - sb.Append(c); - } - } - return sb.ToString(); - } - public string DisplayName { get diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs index c3479598be7466..0770f80b72dbc7 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs @@ -69,6 +69,20 @@ private static bool TryParseAddress(string data, bool expectMultipleAddresses, r Debug.Assert(!string.IsNullOrEmpty(data)); Debug.Assert(index >= 0 && index < data.Length, $"Index out of range: {index}, {data.Length}"); + // Check for CR or LF characters which are not allowed in mail addresses. + // Only scan on the first call (index == data.Length - 1) to avoid repeated O(n) scans + // when parsing multiple addresses from the same string. + if (index == data.Length - 1 && MailBnfHelper.HasCROrLF(data)) + { + if (throwExceptionIfFail) + { + throw new FormatException(SR.MailAddressInvalidFormat); + } + + parseAddressInfo = default; + return false; + } + // Parsed components to be assembled as a MailAddress later string? displayName; diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs index 00da56ee4f646f..95a1beb9c68d3f 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs @@ -147,13 +147,9 @@ public async Task TestMultipleMailDelivery() [Theory] [MemberData(nameof(SendMail_MultiLineDomainLiterals_Data))] - public async Task MultiLineDomainLiterals_Disabled_Throws(string from, string to) + public void MultiLineDomainLiterals_Disabled_Throws(string from, string to) { - Smtp.Credentials = new NetworkCredential("Foo", "Bar"); - - using var msg = new MailMessage(@from, @to, "subject", "body"); - - await SendMail(msg); + Assert.Throws(() => new MailMessage(@from, @to, "subject", "body")); } public static IEnumerable SendMail_MultiLineDomainLiterals_Data() diff --git a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParserTest.cs b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParserTest.cs index b52a60ee14ef2f..1648320bc5a6fb 100644 --- a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParserTest.cs +++ b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParserTest.cs @@ -533,5 +533,39 @@ public void ParseAddresses_WithManyComplexAddresses_ShouldReadCorrectly() Assert.Equal("\" asciin;,oqu o.tesws \"", result[5].User); Assert.Equal("this.test.this", result[5].Host); } + + [Theory] + [InlineData("test\r@example.com")] + [InlineData("test\n@example.com")] + [InlineData("test\r\n@example.com")] + [InlineData("Display\r\nName ")] + [InlineData("test@example\r.com")] + [InlineData("test@example\n.com")] + [InlineData("test@example.com\r\n")] + public void TryParseAddress_WithCROrLF_ShouldThrow(string address) + { + Assert.Throws(() => MailAddressParser.TryParseAddress(address, out _, throwExceptionIfFail: true)); + } + + [Theory] + [InlineData("test\r@example.com")] + [InlineData("test\n@example.com")] + [InlineData("test\r\n@example.com")] + [InlineData("Display\r\nName ")] + [InlineData("test@example\r.com")] + [InlineData("test@example\n.com")] + [InlineData("test@example.com\r\n")] + public void TryParseAddress_WithCROrLF_ShouldReturnFalse(string address) + { + Assert.False(MailAddressParser.TryParseAddress(address, out _, throwExceptionIfFail: false)); + } + + [Fact] + public void ParseMultipleAddresses_WithCROrLF_ShouldThrow() + { + Assert.Throws(() => MailAddressParser.ParseMultipleAddresses("a@b.com, test\r\n@example.com")); + Assert.Throws(() => MailAddressParser.ParseMultipleAddresses("test\n@example.com, a@b.com")); + Assert.Throws(() => MailAddressParser.ParseMultipleAddresses("a@b.com, c@d.com, e\r@f.com")); + } } } diff --git a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs index b0436e77476e8c..0f91df3d42e2ad 100644 --- a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs +++ b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs @@ -36,8 +36,7 @@ public static IEnumerable GetValidEmailTestData() yield return new object[] { "testuser@[mail.com]" }; yield return new object[] { "testuser@[ mail.com] " }; yield return new object[] { "testuser@[mail.com ]" }; - yield return new object[] { "testuser@[mail.com \r\n ]" }; - yield return new object[] { "testuser@[ \r\n mail.com]" }; + yield return new object[] { "testuser@[mail.com ]" }; yield return new object[] { "testuser@[ mail.com]" }; yield return new object[] { "testuser " }; yield return new object[] { "Test\u3044\u3069 User " }; @@ -88,8 +87,8 @@ public static IEnumerable GetValidEmailTestData() yield return new object[] { "\"disp \f lay\" <\"testsome\"@NCLMailTest.com>" }; yield return new object[] { "\"EscapedUnicode \\\u3044\\\u3069 display\" " }; yield return new object[] { "(Unicode \u3044 Comment) <\"testsome\"@NCLMailTest.com>" }; - yield return new object[] { "\"display \r\n name\" <\"folding\"@domain.com>" }; - yield return new object[] { "\"test\r\n test\"@mail.com" }; + yield return new object[] { "\"display name\" <\"folding\"@domain.com>" }; + yield return new object[] { "\"test test\"@mail.com" }; // Email Address Internationalization (EAI) yield return new object[] { "UnicodeUserName \"Test\u3044\u3069\"@NCLMailTest.com" }; yield return new object[] { "<\"EscapedUnicode \\\u3044\\\u3069 User\"@NCLMailTest.com>" }; @@ -125,6 +124,10 @@ public static IEnumerable GetInvalidEmailTestData() yield return new object[] { "Bob \"display\" " }; yield return new object[] { "testuser@[mail.com \r ]" }; yield return new object[] { "testuser@[mail.com \n ]" }; + yield return new object[] { "testuser@[mail.com \r\n ]" }; + yield return new object[] { "testuser@[ \r\n mail.com]" }; + yield return new object[] { "\"display \r\n name\" <\"folding\"@domain.com>" }; + yield return new object[] { "\"test\r\n test\"@mail.com" }; yield return new object[] { "testuser@[mail\u3069.com]" }; // No unicode allowed in square brackets yield return new object[] { "invalid@unicode\uD800.com" }; // D800 is a high surrogate yield return new object[] { "invalid@unicode\uD800.com" }; // D800 is a high surrogate From 697850fe2ca98190c5048f3f233e4ed73d78f4db Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:23:13 +0000 Subject: [PATCH 5/6] Clarify reverse parser CRLF guard comment Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs index 0770f80b72dbc7..af454d22c9cc06 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs @@ -69,8 +69,8 @@ private static bool TryParseAddress(string data, bool expectMultipleAddresses, r Debug.Assert(!string.IsNullOrEmpty(data)); Debug.Assert(index >= 0 && index < data.Length, $"Index out of range: {index}, {data.Length}"); - // Check for CR or LF characters which are not allowed in mail addresses. - // Only scan on the first call (index == data.Length - 1) to avoid repeated O(n) scans + // Reverse parsing starts from the last character of the input in the public entry points. + // Only scan for CR/LF on that initial reverse-parse call to avoid repeated O(n) scans // when parsing multiple addresses from the same string. if (index == data.Length - 1 && MailBnfHelper.HasCROrLF(data)) { From f12da9684842c6d245ceaa43a90a394ae6908023 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:24:21 +0000 Subject: [PATCH 6/6] Tighten MailAddress parser regression coverage Co-authored-by: mrek-msft <188900745+mrek-msft@users.noreply.github.com> --- .../src/System/Net/Mail/MailAddressParser.cs | 19 +++++++++++-------- .../MailAddressParsingTest.cs | 1 - 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs index af454d22c9cc06..ef9e3bb6a30995 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs @@ -70,17 +70,20 @@ private static bool TryParseAddress(string data, bool expectMultipleAddresses, r Debug.Assert(index >= 0 && index < data.Length, $"Index out of range: {index}, {data.Length}"); // Reverse parsing starts from the last character of the input in the public entry points. - // Only scan for CR/LF on that initial reverse-parse call to avoid repeated O(n) scans - // when parsing multiple addresses from the same string. - if (index == data.Length - 1 && MailBnfHelper.HasCROrLF(data)) + // ParseMultipleAddresses decrements index after each parsed address, so only this initial + // reverse-parse call performs the O(n) CR/LF scan for the full input string. + if (index == data.Length - 1) { - if (throwExceptionIfFail) + if (MailBnfHelper.HasCROrLF(data)) { - throw new FormatException(SR.MailAddressInvalidFormat); - } + if (throwExceptionIfFail) + { + throw new FormatException(SR.MailAddressInvalidFormat); + } - parseAddressInfo = default; - return false; + parseAddressInfo = default; + return false; + } } // Parsed components to be assembled as a MailAddress later diff --git a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs index 0f91df3d42e2ad..18e7203df1db28 100644 --- a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs +++ b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs @@ -130,7 +130,6 @@ public static IEnumerable GetInvalidEmailTestData() yield return new object[] { "\"test\r\n test\"@mail.com" }; yield return new object[] { "testuser@[mail\u3069.com]" }; // No unicode allowed in square brackets yield return new object[] { "invalid@unicode\uD800.com" }; // D800 is a high surrogate - yield return new object[] { "invalid@unicode\uD800.com" }; // D800 is a high surrogate yield return new object[] { "invalid\uD800@unicode.com" }; // D800 is a high surrogate yield return new object[] { "\uD800 invalid@unicode.com" }; // D800 is a high surrogate yield return new object[] { null };