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..3c19b90c149e0d 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 @@ -264,10 +264,16 @@ public override string ToString() } else { - return "\"" + DisplayName.Replace("\"", "\\\"") + "\" " + SmtpAddress; + return "\"" + EscapeQuotedStringContent(DisplayName) + "\" " + SmtpAddress; } } + // Escapes backslashes and embedded quotes so the display name forms a + // valid RFC 5322 quoted-string. Order matters: escaping quotes first + // would also escape the backslashes introduced by it. + private static string EscapeQuotedStringContent(string displayName) => + displayName.Replace("\\", "\\\\").Replace("\"", "\\\""); + public override bool Equals([NotNullWhen(true)] object? value) { if (value == null) @@ -299,7 +305,7 @@ internal string Encode(int charsConsumed, bool allowUnicode) //be appended. if (MimeBasePart.IsAscii(_displayName, false) || allowUnicode) { - encodedAddress = "\"" + _displayName + "\""; + encodedAddress = "\"" + EscapeQuotedStringContent(_displayName) + "\""; } else { 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..e08151c9195b44 100644 --- a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressDisplayNameTest.cs +++ b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressDisplayNameTest.cs @@ -28,11 +28,16 @@ public static IEnumerable MailAddressTestDataQuotes() "Display \"Test\" Name", "Display \\\"Test\\\" Name", "\"", + "Display \\ Name", + "Display \\\\ Name", + "C:\\path\\to\\file", + "\\", }; foreach (var displayName in displayNamesWithQuotes) { - yield return new object[]{ Address, displayName, null, $"\"{displayName.Replace("\"", "\\\"")}\" <{Address}>" }; - yield return new object[]{ Address, $"\"{displayName}\"", displayName, $"\"{displayName.Replace("\"", "\\\"")}\" <{Address}>" }; + string escaped = displayName.Replace("\\", "\\\\").Replace("\"", "\\\""); + yield return new object[]{ Address, displayName, null, $"\"{escaped}\" <{Address}>" }; + yield return new object[]{ Address, $"\"{displayName}\"", displayName, $"\"{escaped}\" <{Address}>" }; } yield return new object[]{ Address, null, "", Address }; diff --git a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressEncodeTest.cs b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressEncodeTest.cs index d96fc787ccbd04..016e707be5023c 100644 --- a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressEncodeTest.cs +++ b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressEncodeTest.cs @@ -129,15 +129,15 @@ public void EncodeSingleMailAddress_WithAddressAndLongDisplayNameUnicode_ShouldQ public void EncodeSingleMailAddress_WithAddressAndNonAsciiAndRangeOfChars_ShouldQEncode(Func factory) { MailAddress testAddress = factory("test@example.com", - "\u00AE !#$%&'()+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"); + "\u00AE !\"#$%&'()+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"); string result = testAddress.Encode(0, false); - Assert.Equal("=?utf-8?Q?=C2=AE_=21=23=24=25=26=27=28=29=2B=2C=2D=2E=2F0123456789=3A?=" - + "\r\n =?utf-8?Q?=3B=3C=3D=3E=3F=40ABCDEFGHIJKLMNOPQRSTUVWXYZ=5B=5C=5D=5E=5F?=" - + "\r\n =?utf-8?Q?=60abcdefghijklmnopqrstuvwxyz=7B=7C=7D=7E?= ", result); + Assert.Equal("=?utf-8?Q?=C2=AE_=21=22=23=24=25=26=27=28=29=2B=2C=2D=2E=2F012345678?=" + + "\r\n =?utf-8?Q?9=3A=3B=3C=3D=3E=3F=40ABCDEFGHIJKLMNOPQRSTUVWXYZ=5B=5C=5D?=" + + "\r\n =?utf-8?Q?=5E=5F=60abcdefghijklmnopqrstuvwxyz=7B=7C=7D=7E?= ", result); result = testAddress.Encode(0, true); Assert.Equal( - "\"\u00AE !#$%&'()+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\" " + "\"\u00AE !\\\"#$%&'()+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\" " + "", result); } @@ -214,6 +214,50 @@ public void EncodeMultipleMailAddress_WithManyAddressesThatAreDifferentAndContai + " \"test\u00DC\" ", result); } + public static IEnumerable DisplayNamesWithSpecialChars() + { + // displayName, expected encoded form (without the address suffix). + // Both '"' and '\' are RFC 5322 quoted-string specials and must be + // escaped as quoted-pairs ('\"' and '\\'). + yield return new object[] { "Henry \"The Fonz\" Winkler", "\"Henry \\\"The Fonz\\\" Winkler\"" }; + yield return new object[] { "with \" quote", "\"with \\\" quote\"" }; + yield return new object[] { "with \\ backslash", "\"with \\\\ backslash\"" }; + yield return new object[] { "with \\\" both", "\"with \\\\\\\" both\"" }; + yield return new object[] { "C:\\path\\to\\file", "\"C:\\\\path\\\\to\\\\file\"" }; + yield return new object[] { "\\", "\"\\\\\"" }; + yield return new object[] { "\"", "\"\\\"\"" }; + } + + [Theory] + [MemberData(nameof(DisplayNamesWithSpecialChars))] + public void EncodeSingleMailAddress_WithDisplayNameContainingSpecials_ShouldEscapeAsQuotedPairs(string displayName, string expectedEncodedDisplayName) + { + string expected = $"{expectedEncodedDisplayName} "; + + MailAddress ctorAddress = new MailAddress("test@example.com", displayName); + Assert.Equal(expected, ctorAddress.Encode(0, false)); + Assert.Equal(expected, ctorAddress.Encode(0, true)); + Assert.Equal(expected, ctorAddress.ToString()); + + Assert.True(MailAddress.TryCreate("test@example.com", displayName, out MailAddress tryCreateAddress)); + Assert.Equal(expected, tryCreateAddress.Encode(0, false)); + Assert.Equal(expected, tryCreateAddress.Encode(0, true)); + Assert.Equal(expected, tryCreateAddress.ToString()); + } + + [Theory] + [MemberData(nameof(DisplayNamesWithSpecialChars))] + public void EncodeMultipleMailAddress_WithDisplayNameContainingSpecials_ShouldEscapeAsQuotedPairs(string displayName, string expectedEncodedDisplayName) + { + MailAddress testAddress = new MailAddress("test@example.com", displayName); + MailAddressCollection collection = new MailAddressCollection(); + collection.Add(testAddress); + + string expected = $"{expectedEncodedDisplayName} "; + Assert.Equal(expected, collection.Encode(0, false)); + Assert.Equal(expected, collection.Encode(0, true)); + } + [Fact] public void CustomEncoding_Null() {