From bbc08cb7bf701dcd14a15c906d38e0814e06211a Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 10 Apr 2025 13:57:37 +0200 Subject: [PATCH 1/5] Add STARTTLS tests --- .../Functional/LoopbackServerTestBase.cs | 134 +++++++++++++ .../tests/Functional/LoopbackSmtpServer.cs | 85 ++++++--- .../tests/Functional/SmtpClientTest.cs | 1 - .../tests/Functional/StartTlsTest.cs | 179 ++++++++++++++++++ .../System.Net.Mail.Functional.Tests.csproj | 11 ++ 5 files changed, 383 insertions(+), 27 deletions(-) create mode 100644 src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs create mode 100644 src/libraries/System.Net.Mail/tests/Functional/StartTlsTest.cs diff --git a/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs b/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs new file mode 100644 index 00000000000000..ea3baec63a578d --- /dev/null +++ b/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs @@ -0,0 +1,134 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + + +using System.Net.NetworkInformation; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Net.Mail.Tests; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Mail.Tests +{ + public enum SendMethod + { + Send, + SendAsync, + SendMailAsync + } + + public interface ISendMethodProvider + { + static abstract SendMethod SendMethod { get; } + } + + public struct SyncSendMethod : ISendMethodProvider + { + public static SendMethod SendMethod => SendMethod.Send; + } + + public struct AsyncSendMethod : ISendMethodProvider + { + public static SendMethod SendMethod => SendMethod.SendAsync; + } + + public struct SendMailAsyncMethod : ISendMethodProvider + { + public static SendMethod SendMethod => SendMethod.SendMailAsync; + } + + public abstract class LoopbackServerTestBase : IDisposable + where T : ISendMethodProvider + { + protected LoopbackSmtpServer Server; + protected ITestOutputHelper Output; + + public LoopbackServerTestBase(ITestOutputHelper output) + { + Output = output; + Server = new LoopbackSmtpServer(Output); + } + + private async Task SendMailInternal(SmtpClient client, MailMessage msg) + { + switch (T.SendMethod) + { + case SendMethod.Send: + try + { + client.Send(msg); + return null; + } + catch (Exception ex) + { + return ex; + } + + case SendMethod.SendAsync: + TaskCompletionSource tcs = new TaskCompletionSource(); + SendCompletedEventHandler handler = null!; + handler = (s, e) => + { + client.SendCompleted -= handler; + + if (e.Error != null) + { + tcs.SetResult(e.Error); + } + else if (e.Cancelled) + { + tcs.SetResult(new OperationCanceledException("The operation was canceled.")); + } + else + { + tcs.SetResult(null); + } + }; + client.SendCompleted += handler; + client.SendAsync(msg, tcs); + return await tcs.Task; + + case SendMethod.SendMailAsync: + try + { + await client.SendMailAsync(msg); + return null; + } + catch (Exception ex) + { + return ex; + } + + default: + throw new ArgumentOutOfRangeException(); + } + } + + protected async Task SendMail(SmtpClient client, MailMessage msg) + { + Exception? ex = await SendMailInternal(client, msg); + Assert.Null(ex); + } + + protected async Task SendMail(SmtpClient client, MailMessage msg) where TException : Exception + { + Exception? ex = await SendMailInternal(client, msg); + + if (T.SendMethod != SendMethod.Send && typeof(TException) != typeof(SmtpException)) + { + ex = Assert.IsType(ex).InnerException; + } + + return Assert.IsType(ex); + } + + public void Dispose() + { + Server?.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs b/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs index 6b5d4ab504488e..ed221f72f20d52 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs @@ -12,8 +12,10 @@ using System.Text; using System.Threading; using System.Threading.Tasks; +using System.IO; +using Xunit.Abstractions; -namespace Systen.Net.Mail.Tests +namespace System.Net.Mail.Tests { public class LoopbackSmtpServer : IDisposable { @@ -24,8 +26,10 @@ public class LoopbackSmtpServer : IDisposable public bool SupportSmtpUTF8 = false; public bool AdvertiseNtlmAuthSupport = false; public bool AdvertiseGssapiAuthSupport = false; + public SslServerAuthenticationOptions? SslOptions { get; set; } public NetworkCredential ExpectedGssapiCredential { get; set; } + private ITestOutputHelper? _output; private bool _disposed = false; private readonly Socket _listenSocket; private readonly ConcurrentBag _socketsToDispose; @@ -48,12 +52,15 @@ public class LoopbackSmtpServer : IDisposable public string Password { get; private set; } public string AuthMethodUsed { get; private set; } public ParsedMailMessage Message { get; private set; } + public bool IsEncrypted { get; private set; } + public string TlsHostName { get; private set; } public int ConnectionCount { get; private set; } public int MessagesReceived { get; private set; } - public LoopbackSmtpServer() + public LoopbackSmtpServer(ITestOutputHelper? output = null) { + _output = output; _socketsToDispose = new ConcurrentBag(); _listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _socketsToDispose.Add(_listenSocket); @@ -78,6 +85,7 @@ public LoopbackSmtpServer() private async Task HandleConnectionAsync(Socket socket) { var buffer = new byte[1024].AsMemory(); + Stream stream = new NetworkStream(socket); async ValueTask ReceiveMessageAsync(bool isBody = false) { @@ -87,43 +95,58 @@ async ValueTask ReceiveMessageAsync(bool isBody = false) int received = 0; do { - int read = await socket.ReceiveAsync(buffer.Slice(received), SocketFlags.None); + int read = await stream.ReadAsync(buffer.Slice(received)); + if (read == 0) return null; received += read; } while (received < suffix || !buffer.Slice(received - suffix, suffix).Span.SequenceEqual(terminator.Span)); MessagesReceived++; - return Encoding.UTF8.GetString(buffer.Span.Slice(0, received - suffix)); + string message = Encoding.UTF8.GetString(buffer.Span.Slice(0, received - suffix)); + _output?.WriteLine($"Client> {message}"); + return message; } + async ValueTask SendMessageAsync(string text) { var bytes = buffer.Slice(0, Encoding.UTF8.GetBytes(text, buffer.Span) + 2); bytes.Span[^2] = (byte)'\r'; bytes.Span[^1] = (byte)'\n'; - await socket.SendAsync(bytes, SocketFlags.None); + + _output?.WriteLine($"Server> {text}"); + await stream.WriteAsync(bytes); + await stream.FlushAsync(); } try { OnConnected?.Invoke(socket); await SendMessageAsync("220 localhost"); + bool isFirstMessage = true; + + while (await ReceiveMessageAsync() is string message && message != null) + { + Debug.Assert(!isFirstMessage || (message.ToLower().StartsWith("helo ") || message.ToLower().StartsWith("ehlo ")), "Expected the first message to be HELO/EHLO"); + isFirstMessage = false; - string message = await ReceiveMessageAsync(); - Debug.Assert(message.ToLower().StartsWith("helo ") || message.ToLower().StartsWith("ehlo ")); - ClientDomain = message.Substring(5).ToLower(); - OnCommandReceived?.Invoke(message.Substring(0, 4), ClientDomain); - OnHelloReceived?.Invoke(ClientDomain); + if (message.ToLower().StartsWith("helo ") || message.ToLower().StartsWith("ehlo ")) + { + ClientDomain = message.Substring(5).ToLower(); + OnCommandReceived?.Invoke(message.Substring(0, 4), ClientDomain); + OnHelloReceived?.Invoke(ClientDomain); + + await SendMessageAsync("250-localhost, mock server here"); + if (SupportSmtpUTF8) await SendMessageAsync("250-SMTPUTF8"); + if (SslOptions != null && stream is not SslStream) await SendMessageAsync("250-STARTTLS"); + await SendMessageAsync( + "250 AUTH PLAIN LOGIN" + + (AdvertiseNtlmAuthSupport ? " NTLM" : "") + + (AdvertiseGssapiAuthSupport ? " GSSAPI" : "")); - await SendMessageAsync("250-localhost, mock server here"); - if (SupportSmtpUTF8) await SendMessageAsync("250-SMTPUTF8"); - await SendMessageAsync( - "250 AUTH PLAIN LOGIN" + - (AdvertiseNtlmAuthSupport ? " NTLM" : "") + - (AdvertiseGssapiAuthSupport ? " GSSAPI" : "")); + continue; + } - while ((message = await ReceiveMessageAsync()) != null) - { int colonIndex = message.IndexOf(':'); string command = colonIndex == -1 ? message : message.Substring(0, colonIndex); string argument = command.Length == message.Length ? string.Empty : message.Substring(colonIndex + 1).Trim(); @@ -201,6 +224,23 @@ await SendMessageAsync( switch (command.ToUpper()) { + case "STARTTLS": + if (SslOptions == null || stream is SslStream) + { + await SendMessageAsync("454 TLS not available"); + break; + } + await SendMessageAsync("220 Ready to start TLS"); + + // Upgrade connection to TLS + var sslStream = new SslStream(stream); + await sslStream.AuthenticateAsServerAsync(SslOptions); + IsEncrypted = true; + TlsHostName = sslStream.TargetHostName; + + stream = sslStream; + break; + case "MAIL FROM": MailFrom = argument; await SendMessageAsync("250 Ok"); @@ -233,14 +273,7 @@ await SendMessageAsync( catch { } finally { - try - { - socket.Shutdown(SocketShutdown.Both); - } - finally - { - socket?.Close(); - } + stream.Dispose(); } } diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs index c8c5734b04cf44..605eb867f8a210 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs @@ -18,7 +18,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.RemoteExecutor; -using Systen.Net.Mail.Tests; using System.Net.Test.Common; using Xunit; diff --git a/src/libraries/System.Net.Mail/tests/Functional/StartTlsTest.cs b/src/libraries/System.Net.Mail/tests/Functional/StartTlsTest.cs new file mode 100644 index 00000000000000..b6800c25789f90 --- /dev/null +++ b/src/libraries/System.Net.Mail/tests/Functional/StartTlsTest.cs @@ -0,0 +1,179 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.NetworkInformation; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Net.Mail.Tests; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Mail.Tests +{ + // Common test setup to share across test cases. + public class CertificateSetup : IDisposable + { + public readonly X509Certificate2 serverCert; + public readonly X509Certificate2Collection serverChain; + public readonly SslStreamCertificateContext serverCertContext; + + public CertificateSetup() + { + (serverCert, serverChain) = System.Net.Test.Common.Configuration.Certificates.GenerateCertificates("localhost", nameof(SmtpClientStartTlsTest<>)); + serverCertContext = SslStreamCertificateContext.Create(serverCert, serverChain); + } + + public void Dispose() + { + serverCert.Dispose(); + foreach (var c in serverChain) + { + c.Dispose(); + } + } + } + + public abstract class SmtpClientStartTlsTest : LoopbackServerTestBase + where TSendMethod : ISendMethodProvider + { + private CertificateSetup _certificateSetup; + private Func? _serverCertValidationCallback; + + public SmtpClientStartTlsTest(ITestOutputHelper output, CertificateSetup certificateSetup) : base(output) + { + _certificateSetup = certificateSetup; + Server.SslOptions = new SslServerAuthenticationOptions + { + ServerCertificateContext = _certificateSetup.serverCertContext, + ClientCertificateRequired = false, + }; + +#pragma warning disable SYSLIB0014 // ServicePointManager is obsolete + ServicePointManager.ServerCertificateValidationCallback = ServerCertValidationCallback; +#pragma warning restore SYSLIB0014 // ServicePointManager is obsolete + } + + [Fact] + public async Task EnableSslServerSupports_UsesTls() + { + using SmtpClient client = Server.CreateClient(); + _serverCertValidationCallback = (cert, chain, errors) => + { + return true; + }; + + client.Credentials = new NetworkCredential("foo", "bar"); + client.EnableSsl = true; + MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); + + await SendMail(client, msg); + Assert.True(Server.IsEncrypted, "TLS was not negotiated."); + Assert.Equal(client.Host, Server.TlsHostName); + } + + [Fact] + public async Task EnableSsl_NoServerSupport_NoTls() + { + using SmtpClient client = Server.CreateClient(); + Server.SslOptions = null; + + client.Credentials = new NetworkCredential("foo", "bar"); + client.EnableSsl = true; + MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); + + await SendMail(client, msg); + } + + [Fact] + public async Task DisableSslServerSupport_NoTls() + { + using SmtpClient client = Server.CreateClient(); + + client.Credentials = new NetworkCredential("foo", "bar"); + client.EnableSsl = false; + MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); + + await SendMail(client, msg); + Assert.False(Server.IsEncrypted, "TLS was negotiated when it should not have been."); + } + + [Fact] + public async Task AuthenticationException_Propagates() + { + using SmtpClient client = Server.CreateClient(); + _serverCertValidationCallback = (cert, chain, errors) => + { + return false; // force auth errors + }; + + client.Credentials = new NetworkCredential("foo", "bar"); + client.EnableSsl = true; + MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); + + await SendMail(client, msg); + } + + [Fact] + public async Task ClientCertificateRequired_Sent() + { + Server.SslOptions.ClientCertificateRequired = true; + X509Certificate2 clientCert = _certificateSetup.serverCert; // use the server cert as a client cert for testing + X509Certificate2? receivedClientCert = null; + Server.SslOptions.RemoteCertificateValidationCallback = (sender, cert, chain, errors) => + { + receivedClientCert = cert as X509Certificate2; + return true; + }; + + using SmtpClient client = Server.CreateClient(); + _serverCertValidationCallback = (cert, chain, errors) => + { + return true; + }; + + client.Credentials = new NetworkCredential("foo", "bar"); + client.EnableSsl = true; + client.ClientCertificates.Add(clientCert); + + MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); + + await SendMail(client, msg); + Assert.True(Server.IsEncrypted, "TLS was not negotiated."); + Assert.Equal(clientCert, receivedClientCert); + } + + private bool ServerCertValidationCallback(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors sslPolicyErrors) + { + if (_serverCertValidationCallback != null) + { + return _serverCertValidationCallback((X509Certificate2)certificate!, chain!, sslPolicyErrors); + } + + // Default validation: check if the certificate is valid. + return sslPolicyErrors == SslPolicyErrors.None; + } + } + + // since the tests change global state (ServicePointManager.ServerCertificateValidationCallback), we need to run them in isolation + + [Collection(nameof(DisableParallelization))] + public class StartTlsTest_Send : SmtpClientStartTlsTest, IClassFixture + { + public StartTlsTest_Send(ITestOutputHelper output, CertificateSetup certificateSetup) : base(output, certificateSetup) { } + } + + [Collection(nameof(DisableParallelization))] + public class StartTlsTest_SendAsync : SmtpClientStartTlsTest, IClassFixture + { + public StartTlsTest_SendAsync(ITestOutputHelper output, CertificateSetup certificateSetup) : base(output, certificateSetup) { } + } + + [Collection(nameof(DisableParallelization))] + public class StartTlsTest_SendMailAsync : SmtpClientStartTlsTest, IClassFixture + { + public StartTlsTest_SendMailAsync(ITestOutputHelper output, CertificateSetup certificateSetup) : base(output, certificateSetup) { } + } +} \ No newline at end of file diff --git a/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj b/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj index 28ea42dedc3795..76f0a6cd2692e9 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj +++ b/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj @@ -21,6 +21,12 @@ + + + + + + + From 8f319070750d51ea88a20d0e0f9c013a8a01571a Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 10 Apr 2025 17:21:50 +0200 Subject: [PATCH 2/5] Partition tests from SmtpClientTest class by area --- .../Functional/LoopbackServerTestBase.cs | 64 ++-- .../tests/Functional/SmtpClientAuthTest.cs | 76 +++++ .../Functional/SmtpClientSendMailTest.cs | 189 ++++++++++++ .../SmtpClientSpecifiedPickupDirectoryTest.cs | 107 +++++++ .../tests/Functional/SmtpClientTest.cs | 281 ------------------ .../{StartTlsTest.cs => SmtpClientTlsTest.cs} | 57 ++-- .../System.Net.Mail.Functional.Tests.csproj | 5 +- 7 files changed, 447 insertions(+), 332 deletions(-) create mode 100644 src/libraries/System.Net.Mail/tests/Functional/SmtpClientAuthTest.cs create mode 100644 src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs create mode 100644 src/libraries/System.Net.Mail/tests/Functional/SmtpClientSpecifiedPickupDirectoryTest.cs rename src/libraries/System.Net.Mail/tests/Functional/{StartTlsTest.cs => SmtpClientTlsTest.cs} (72%) diff --git a/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs b/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs index ea3baec63a578d..b9571fba916a53 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - using System.Net.NetworkInformation; using System.Net.Security; using System.Security.Authentication; @@ -44,8 +43,18 @@ public struct SendMailAsyncMethod : ISendMethodProvider public abstract class LoopbackServerTestBase : IDisposable where T : ISendMethodProvider { - protected LoopbackSmtpServer Server; - protected ITestOutputHelper Output; + protected LoopbackSmtpServer Server { get; private set; } + protected ITestOutputHelper Output { get; private set; } + + private SmtpClient _smtp; + + protected SmtpClient Smtp + { + get + { + return _smtp ??= Server.CreateClient(); + } + } public LoopbackServerTestBase(ITestOutputHelper output) { @@ -53,19 +62,19 @@ public LoopbackServerTestBase(ITestOutputHelper output) Server = new LoopbackSmtpServer(Output); } - private async Task SendMailInternal(SmtpClient client, MailMessage msg) + private Task SendMailInternal(MailMessage msg) { switch (T.SendMethod) { case SendMethod.Send: try { - client.Send(msg); - return null; + Smtp.Send(msg); + return Task.FromResult(null); } catch (Exception ex) { - return ex; + return Task.FromResult(ex); } case SendMethod.SendAsync: @@ -73,7 +82,7 @@ public LoopbackServerTestBase(ITestOutputHelper output) SendCompletedEventHandler handler = null!; handler = (s, e) => { - client.SendCompleted -= handler; + Smtp.SendCompleted -= handler; if (e.Error != null) { @@ -88,19 +97,33 @@ public LoopbackServerTestBase(ITestOutputHelper output) tcs.SetResult(null); } }; - client.SendCompleted += handler; - client.SendAsync(msg, tcs); - return await tcs.Task; + Smtp.SendCompleted += handler; + try + { + Smtp.SendAsync(msg, tcs); + return tcs.Task; + } + catch (Exception ex) + { + Smtp.SendCompleted -= handler; + return Task.FromResult(ex); + } case SendMethod.SendMailAsync: try { - await client.SendMailAsync(msg); - return null; + return Smtp.SendMailAsync(msg).ContinueWith(t => + { + if (t.IsFaulted) + { + return t.Exception?.InnerException; + } + return null; + }); } catch (Exception ex) { - return ex; + return Task.FromResult(ex); } default: @@ -108,15 +131,15 @@ public LoopbackServerTestBase(ITestOutputHelper output) } } - protected async Task SendMail(SmtpClient client, MailMessage msg) + protected async Task SendMail(MailMessage msg) { - Exception? ex = await SendMailInternal(client, msg); + Exception? ex = await SendMailInternal(msg); Assert.Null(ex); } - protected async Task SendMail(SmtpClient client, MailMessage msg) where TException : Exception + protected async Task SendMail(MailMessage msg) where TException : Exception { - Exception? ex = await SendMailInternal(client, msg); + Exception? ex = await SendMailInternal(msg); if (T.SendMethod != SendMethod.Send && typeof(TException) != typeof(SmtpException)) { @@ -126,8 +149,11 @@ protected async Task SendMail(SmtpClient client, MailMes return Assert.IsType(ex); } - public void Dispose() + protected static string GetClientDomain() => IPGlobalProperties.GetIPGlobalProperties().HostName.Trim().ToLower(); + + public virtual void Dispose() { + _smtp?.Dispose(); Server?.Dispose(); } } diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAuthTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAuthTest.cs new file mode 100644 index 00000000000000..e56362ff3b2984 --- /dev/null +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAuthTest.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.NetworkInformation; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Net.Mail.Tests; +using System.Net.Test.Common; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Mail.Tests +{ + public abstract class SmtpClientAuthTest : LoopbackServerTestBase + where TSendMethod : ISendMethodProvider + { + public static bool IsNtlmInstalled => Capability.IsNtlmInstalled(); + + public SmtpClientAuthTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] // NTLM support required, see https://github.com/dotnet/runtime/issues/25827 + [SkipOnCoreClr("System.Net.Tests are flaky and/or long running: https://github.com/dotnet/runtime/issues/131", ~RuntimeConfiguration.Release)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/131", TestRuntimes.Mono)] // System.Net.Tests are flaky and/or long running + public async Task TestCredentialsCopyInAsyncContext() + { + MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); + + CredentialCache cache = new CredentialCache(); + cache.Add("localhost", Server.Port, "NTLM", CredentialCache.DefaultNetworkCredentials); + + Smtp.Credentials = cache; + + // The mock server doesn't actually understand NTLM, but still advertises support for it + Server.AdvertiseNtlmAuthSupport = true; + await SendMail(msg); + + Assert.Equal("NTLM", Server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); + } + + [ConditionalFact(nameof(IsNtlmInstalled))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/65678", TestPlatforms.OSX | TestPlatforms.iOS | TestPlatforms.MacCatalyst)] + public async Task TestGssapiAuthentication() + { + Server.AdvertiseGssapiAuthSupport = true; + Server.ExpectedGssapiCredential = new NetworkCredential("foo", "bar"); + Smtp.Credentials = Server.ExpectedGssapiCredential; + MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); + + await SendMail(msg); + + Assert.Equal("GSSAPI", Server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); + } + + } + + public class SmtpClientAuthTest_Send : SmtpClientAuthTest + { + public SmtpClientAuthTest_Send(ITestOutputHelper output) : base(output) { } + } + + public class SmtpClientAuthTest_SendAsync : SmtpClientAuthTest + { + public SmtpClientAuthTest_SendAsync(ITestOutputHelper output) : base(output) { } + } + + public class SmtpClientAuthTest_SendMailAsync : SmtpClientAuthTest + { + public SmtpClientAuthTest_SendMailAsync(ITestOutputHelper output) : base(output) { } + } +} \ No newline at end of file diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs new file mode 100644 index 00000000000000..6309a216328813 --- /dev/null +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs @@ -0,0 +1,189 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.NetworkInformation; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Net.Mail.Tests; +using System.IO; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Mail.Tests +{ + public abstract class SmtpClientSendMailTest : LoopbackServerTestBase where T : ISendMethodProvider + { + public SmtpClientSendMailTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task Message_Null() + { + await SendMail(null); + } + + [Fact] + public async Task Network_Host_Whitespace() + { + Smtp.Host = " \r\n "; + await SendMail(new MailMessage("mono@novell.com", "everyone@novell.com", "introduction", "hello")); + } + + [Fact] + public async Task ServerDoesntExist_Throws() + { + Smtp.Host = Guid.NewGuid().ToString("N"); + await SendMail(new MailMessage("mono@novell.com", "everyone@novell.com", "introduction", "hello")); + } + + [Theory] + [InlineData("howdydoo")] + [InlineData("")] + [InlineData(null)] + [SkipOnCoreClr("System.Net.Tests are flaky and/or long running: https://github.com/dotnet/runtime/issues/131", ~RuntimeConfiguration.Release)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/131", TestRuntimes.Mono)] // System.Net.Tests are flaky and/or long running + public async Task MailDelivery(string body) + { + Smtp.Credentials = new NetworkCredential("foo", "bar"); + MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", body); + + await SendMail(msg).WaitAsync(TimeSpan.FromSeconds(30)); + + Assert.Equal("", Server.MailFrom); + Assert.Equal("", Server.MailTo); + Assert.Equal("hello", Server.Message.Subject); + Assert.Equal(body ?? "", Server.Message.Body); + Assert.Equal(GetClientDomain(), Server.ClientDomain); + Assert.Equal("foo", Server.Username); + Assert.Equal("bar", Server.Password); + Assert.Equal("LOGIN", Server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] // Received subjectText. + [InlineData(true, false)] + [InlineData(true, true)] // Received subjectBase64. If subjectText is received, the test fails, and the results are inconsistent with those of synchronous methods. + public async Task SendMail_DeliveryFormat_SubjectEncoded(bool useSevenBit, bool useSmtpUTF8) + { + // If the server support `SMTPUTF8` and use `SmtpDeliveryFormat.International`, the server should received this subject. + const string subjectText = "Test \u6d4b\u8bd5 Contain \u5305\u542b UTF8"; + + // If the server does not support `SMTPUTF8` or use `SmtpDeliveryFormat.SevenBit`, the server should received this subject. + const string subjectBase64 = "=?utf-8?B?VGVzdCDmtYvor5UgQ29udGFpbiDljIXlkKsgVVRGOA==?="; + + // Setting up Server Support for `SMTPUTF8`. + Server.SupportSmtpUTF8 = useSmtpUTF8; + + if (useSevenBit) + { + // Subject will be encoded by Base64. + Smtp.DeliveryFormat = SmtpDeliveryFormat.SevenBit; + } + else + { + // If the server supports `SMTPUTF8`, subject will not be encoded. Otherwise, subject will be encoded by Base64. + Smtp.DeliveryFormat = SmtpDeliveryFormat.International; + } + + MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", subjectText, "hello \u9ad8\u575a\u679c"); + msg.HeadersEncoding = msg.BodyEncoding = msg.SubjectEncoding = System.Text.Encoding.UTF8; + + await SendMail(msg); + + if (useSevenBit || !useSmtpUTF8) + { + Assert.Equal(subjectBase64, Server.Message.Subject); + } + else + { + Assert.Equal(subjectText, Server.Message.Subject); + } + } + + [Fact] + public async Task SendQUITOnDispose() + { + bool quitMessageReceived = false; + using ManualResetEventSlim quitReceived = new ManualResetEventSlim(); + Server.OnQuitReceived += _ => + { + quitMessageReceived = true; + quitReceived.Set(); + }; + + Smtp.Credentials = new NetworkCredential("Foo", "Bar"); + MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); + await SendMail(msg); + Assert.False(quitMessageReceived, "QUIT received"); + Smtp.Dispose(); + + // There is a latency between send/receive. + quitReceived.Wait(TimeSpan.FromSeconds(30)); + Assert.True(quitMessageReceived, "QUIT message not received"); + } + + [Fact] + public async Task TestMultipleMailDelivery() + { + Smtp.Timeout = 10000; + Smtp.Credentials = new NetworkCredential("foo", "bar"); + MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); + + for (var i = 0; i < 5; i++) + { + await SendMail(msg); + + Assert.Equal("", Server.MailFrom); + Assert.Equal("", Server.MailTo); + Assert.Equal("hello", Server.Message.Subject); + Assert.Equal("howdydoo", Server.Message.Body); + Assert.Equal(GetClientDomain(), Server.ClientDomain); + Assert.Equal("foo", Server.Username); + Assert.Equal("bar", Server.Password); + Assert.Equal("LOGIN", Server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); + } + } + + [Theory] + [MemberData(nameof(SendMail_MultiLineDomainLiterals_Data))] + public async Task 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); + } + + public static IEnumerable SendMail_MultiLineDomainLiterals_Data() + { + foreach (string address in new[] { "foo@[\r\n bar]", "foo@[bar\r\n ]", "foo@[bar\r\n baz]" }) + { + yield return new object[] { address, "foo@example.com" }; + yield return new object[] { "foo@example.com", address }; + } + } + } + + public class SmtpClientSendMailTest_Send : SmtpClientSendMailTest + { + public SmtpClientSendMailTest_Send(ITestOutputHelper output) : base(output) { } + } + + public class SmtpClientSendMailTest_SendAsync : SmtpClientSendMailTest + { + public SmtpClientSendMailTest_SendAsync(ITestOutputHelper output) : base(output) { } + } + + public class SmtpClientSendMailTest_SendMailAsync : SmtpClientSendMailTest + { + public SmtpClientSendMailTest_SendMailAsync(ITestOutputHelper output) : base(output) { } + } +} \ No newline at end of file diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSpecifiedPickupDirectoryTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSpecifiedPickupDirectoryTest.cs new file mode 100644 index 00000000000000..92021692d8eb16 --- /dev/null +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSpecifiedPickupDirectoryTest.cs @@ -0,0 +1,107 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.NetworkInformation; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Net.Mail.Tests; +using System.IO; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Mail.Tests +{ + public abstract class SmtpClientSpecifiedPickupDirectoryTest : LoopbackServerTestBase where T : ISendMethodProvider + { + class FileCleanupProvider : FileCleanupTestBase + { + // expose protected member + public new string TestDirectory => base.TestDirectory; + } + + FileCleanupProvider _fileCleanupProvider = new FileCleanupProvider(); + + public SmtpClientSpecifiedPickupDirectoryTest(ITestOutputHelper output) : base(output) + { + + } + + private string TempFolder + { + get + { + return _fileCleanupProvider.TestDirectory; + } + } + + public override void Dispose() + { + _fileCleanupProvider.Dispose(); + } + + [Fact] + public async Task Send() + { + Smtp.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory; + Smtp.PickupDirectoryLocation = TempFolder; + await SendMail(new MailMessage("mono@novell.com", "everyone@novell.com", "introduction", "hello")); + + string[] files = Directory.GetFiles(TempFolder, "*"); + Assert.Equal(1, files.Length); + Assert.Equal(".eml", Path.GetExtension(files[0])); + } + + [Fact] + public async Task Send_SpecifiedPickupDirectory_MessageBodyDoesNotEncodeForTransport() + { + // This test verifies that a line fold which results in a dot appearing as the first character of + // a new line does not get dot-stuffed when the delivery method is pickup. To do so, it relies on + // folding happening at a precise location. If folding implementation details change, this test will + // likely fail and need to be updated accordingly. + + string padding = new string('a', 65); + + Smtp.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory; + Smtp.PickupDirectoryLocation = TempFolder; + + await SendMail(new MailMessage("mono@novell.com", "everyone@novell.com", "introduction", padding + ".")); + + string[] files = Directory.GetFiles(TempFolder, "*"); + Assert.Equal(1, files.Length); + Assert.Equal(".eml", Path.GetExtension(files[0])); + + string message = File.ReadAllText(files[0]); + Assert.EndsWith($"{padding}=\r\n.\r\n", message); + } + + [Theory] + [InlineData("some_path_not_exist")] + [InlineData("")] + [InlineData(null)] + [InlineData("\0abc")] + public async Task Send_SpecifiedPickupDirectoryInvalid(string location) + { + Smtp.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory; + Smtp.PickupDirectoryLocation = location; + await SendMail(new MailMessage("mono@novell.com", "everyone@novell.com", "introduction", "hello")); + } + } + + public class SmtpClientSpecifiedPickupDirectoryTest_Send : SmtpClientSpecifiedPickupDirectoryTest + { + public SmtpClientSpecifiedPickupDirectoryTest_Send(ITestOutputHelper output) : base(output) { } + } + + public class SmtpClientSpecifiedPickupDirectoryTest_SendAsync : SmtpClientSpecifiedPickupDirectoryTest + { + public SmtpClientSpecifiedPickupDirectoryTest_SendAsync(ITestOutputHelper output) : base(output) { } + } + + public class SmtpClientSpecifiedPickupDirectoryTest_SendMailAsync : SmtpClientSpecifiedPickupDirectoryTest + { + public SmtpClientSpecifiedPickupDirectoryTest_SendMailAsync(ITestOutputHelper output) : base(output) { } + } +} \ No newline at end of file diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs index 605eb867f8a210..e69a9db6a5ed27 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs @@ -28,8 +28,6 @@ public class SmtpClientTest : FileCleanupTestBase { private SmtpClient _smtp; - public static bool IsNtlmInstalled => Capability.IsNtlmInstalled(); - private SmtpClient Smtp { get @@ -194,71 +192,12 @@ public void Port_Value_Invalid(int value) Assert.Throws(() => Smtp.Port = value); } - [Fact] - public void Send_Message_Null() - { - Assert.Throws(() => Smtp.Send(null)); - } - [Fact] public void Send_Network_Host_Null() { Assert.Throws(() => Smtp.Send("mono@novell.com", "everyone@novell.com", "introduction", "hello")); } - [Fact] - public void Send_Network_Host_Whitespace() - { - Smtp.Host = " \r\n "; - Assert.Throws(() => Smtp.Send("mono@novell.com", "everyone@novell.com", "introduction", "hello")); - } - - [Fact] - public void Send_SpecifiedPickupDirectory() - { - Smtp.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory; - Smtp.PickupDirectoryLocation = TempFolder; - Smtp.Send("mono@novell.com", "everyone@novell.com", "introduction", "hello"); - - string[] files = Directory.GetFiles(TempFolder, "*"); - Assert.Equal(1, files.Length); - Assert.Equal(".eml", Path.GetExtension(files[0])); - } - - [Fact] - public void Send_SpecifiedPickupDirectory_MessageBodyDoesNotEncodeForTransport() - { - // This test verifies that a line fold which results in a dot appearing as the first character of - // a new line does not get dot-stuffed when the delivery method is pickup. To do so, it relies on - // folding happening at a precise location. If folding implementation details change, this test will - // likely fail and need to be updated accordingly. - - string padding = new string('a', 65); - - Smtp.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory; - Smtp.PickupDirectoryLocation = TempFolder; - Smtp.Send("mono@novell.com", "everyone@novell.com", "introduction", padding + "."); - - string[] files = Directory.GetFiles(TempFolder, "*"); - Assert.Equal(1, files.Length); - Assert.Equal(".eml", Path.GetExtension(files[0])); - - string message = File.ReadAllText(files[0]); - Assert.EndsWith($"{padding}=\r\n.\r\n", message); - } - - [Theory] - [InlineData("some_path_not_exist")] - [InlineData("")] - [InlineData(null)] - [InlineData("\0abc")] - public void Send_SpecifiedPickupDirectoryInvalid(string location) - { - Smtp.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory; - Smtp.PickupDirectoryLocation = location; - Assert.Throws(() => Smtp.Send("mono@novell.com", "everyone@novell.com", "introduction", "hello")); - } - [Theory] [InlineData(0)] [InlineData(50)] @@ -340,106 +279,6 @@ public void TestZeroTimeout() } } - [Theory] - [InlineData("howdydoo")] - [InlineData("")] - [InlineData(null)] - [SkipOnCoreClr("System.Net.Tests are flaky and/or long running: https://github.com/dotnet/runtime/issues/131", ~RuntimeConfiguration.Release)] - [ActiveIssue("https://github.com/dotnet/runtime/issues/131", TestRuntimes.Mono)] // System.Net.Tests are flaky and/or long running - public async Task TestMailDeliveryAsync(string body) - { - using var server = new LoopbackSmtpServer(); - using SmtpClient client = server.CreateClient(); - MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", body); - - await client.SendMailAsync(msg).WaitAsync(TimeSpan.FromSeconds(30)); - - Assert.Equal("", server.MailFrom); - Assert.Equal("", server.MailTo); - Assert.Equal("hello", server.Message.Subject); - Assert.Equal(body ?? "", server.Message.Body); - Assert.Equal(GetClientDomain(), server.ClientDomain); - } - - [Fact] - [PlatformSpecific(TestPlatforms.Windows)] // NTLM support required, see https://github.com/dotnet/runtime/issues/25827 - [SkipOnCoreClr("System.Net.Tests are flaky and/or long running: https://github.com/dotnet/runtime/issues/131", ~RuntimeConfiguration.Release)] - [ActiveIssue("https://github.com/dotnet/runtime/issues/131", TestRuntimes.Mono)] // System.Net.Tests are flaky and/or long running - public async Task TestCredentialsCopyInAsyncContext() - { - using var server = new LoopbackSmtpServer(); - using SmtpClient client = server.CreateClient(); - MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); - - CredentialCache cache = new CredentialCache(); - cache.Add("localhost", server.Port, "NTLM", CredentialCache.DefaultNetworkCredentials); - - client.Credentials = cache; - - // The mock server doesn't actually understand NTLM, but still advertises support for it - server.AdvertiseNtlmAuthSupport = true; - await Assert.ThrowsAsync(async () => await client.SendMailAsync(msg)); - - Assert.Equal("NTLM", server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); - } - - - [Theory] - [InlineData(false, false, false)] - [InlineData(false, false, true)] // Received subjectText. - [InlineData(false, true, false)] - [InlineData(false, true, true)] - [InlineData(true, false, false)] - [InlineData(true, false, true)] // Received subjectText. - [InlineData(true, true, false)] - [InlineData(true, true, true)] // Received subjectBase64. If subjectText is received, the test fails, and the results are inconsistent with those of synchronous methods. - public void SendMail_DeliveryFormat_SubjectEncoded(bool useAsyncSend, bool useSevenBit, bool useSmtpUTF8) - { - // If the server support `SMTPUTF8` and use `SmtpDeliveryFormat.International`, the server should received this subject. - const string subjectText = "Test \u6d4b\u8bd5 Contain \u5305\u542b UTF8"; - - // If the server does not support `SMTPUTF8` or use `SmtpDeliveryFormat.SevenBit`, the server should received this subject. - const string subjectBase64 = "=?utf-8?B?VGVzdCDmtYvor5UgQ29udGFpbiDljIXlkKsgVVRGOA==?="; - - using var server = new LoopbackSmtpServer(); - using SmtpClient client = server.CreateClient(); - - // Setting up Server Support for `SMTPUTF8`. - server.SupportSmtpUTF8 = useSmtpUTF8; - - if (useSevenBit) - { - // Subject will be encoded by Base64. - client.DeliveryFormat = SmtpDeliveryFormat.SevenBit; - } - else - { - // If the server supports `SMTPUTF8`, subject will not be encoded. Otherwise, subject will be encoded by Base64. - client.DeliveryFormat = SmtpDeliveryFormat.International; - } - - MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", subjectText, "hello \u9ad8\u575a\u679c"); - msg.HeadersEncoding = msg.BodyEncoding = msg.SubjectEncoding = System.Text.Encoding.UTF8; - - if (useAsyncSend) - { - client.SendMailAsync(msg).Wait(); - } - else - { - client.Send(msg); - } - - if (useSevenBit || !useSmtpUTF8) - { - Assert.Equal(subjectBase64, server.Message.Subject); - } - else - { - Assert.Equal(subjectText, server.Message.Subject); - } - } - [Fact] public void SendMailAsync_CanBeCanceled_CancellationToken_SetAlready() { @@ -492,125 +331,5 @@ public async Task SendMailAsync_CanBeCanceled_CancellationToken() } private static string GetClientDomain() => IPGlobalProperties.GetIPGlobalProperties().HostName.Trim().ToLower(); - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task SendMail_SendQUITOnDispose(bool asyncSend) - { - bool quitMessageReceived = false; - using ManualResetEventSlim quitReceived = new ManualResetEventSlim(); - using var server = new LoopbackSmtpServer(); - server.OnQuitReceived += _ => - { - quitMessageReceived = true; - quitReceived.Set(); - }; - - using (SmtpClient client = server.CreateClient()) - { - client.Credentials = new NetworkCredential("Foo", "Bar"); - MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); - if (asyncSend) - { - await client.SendMailAsync(msg).WaitAsync(TimeSpan.FromSeconds(30)); - } - else - { - client.Send(msg); - } - Assert.False(quitMessageReceived, "QUIT received"); - } - - // There is a latency between send/receive. - quitReceived.Wait(TimeSpan.FromSeconds(30)); - Assert.True(quitMessageReceived, "QUIT message not received"); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task TestMultipleMailDelivery(bool asyncSend) - { - using var server = new LoopbackSmtpServer(); - using SmtpClient client = server.CreateClient(); - client.Timeout = 10000; - client.Credentials = new NetworkCredential("foo", "bar"); - MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); - - for (var i = 0; i < 5; i++) - { - if (asyncSend) - { - using var cts = new CancellationTokenSource(10000); - await client.SendMailAsync(msg, cts.Token); - } - else - { - client.Send(msg); - } - - Assert.Equal("", server.MailFrom); - Assert.Equal("", server.MailTo); - Assert.Equal("hello", server.Message.Subject); - Assert.Equal("howdydoo", server.Message.Body); - Assert.Equal(GetClientDomain(), server.ClientDomain); - Assert.Equal("foo", server.Username); - Assert.Equal("bar", server.Password); - Assert.Equal("LOGIN", server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); - } - } - - [ConditionalFact(nameof(IsNtlmInstalled))] - [ActiveIssue("https://github.com/dotnet/runtime/issues/65678", TestPlatforms.OSX | TestPlatforms.iOS | TestPlatforms.MacCatalyst)] - public void TestGssapiAuthentication() - { - using var server = new LoopbackSmtpServer(); - server.AdvertiseGssapiAuthSupport = true; - server.ExpectedGssapiCredential = new NetworkCredential("foo", "bar"); - using SmtpClient client = server.CreateClient(); - client.Credentials = server.ExpectedGssapiCredential; - MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); - - client.Send(msg); - - Assert.Equal("GSSAPI", server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); - } - - [Theory] - [MemberData(nameof(SendMail_MultiLineDomainLiterals_Data))] - public async Task SendMail_MultiLineDomainLiterals_Disabled_Throws(string from, string to, bool asyncSend) - { - using var server = new LoopbackSmtpServer(); - - using SmtpClient client = server.CreateClient(); - client.Credentials = new NetworkCredential("Foo", "Bar"); - - using var msg = new MailMessage(@from, @to, "subject", "body"); - - await Assert.ThrowsAsync(async () => - { - if (asyncSend) - { - await client.SendMailAsync(msg).WaitAsync(TimeSpan.FromSeconds(30)); - } - else - { - client.Send(msg); - } - }); - } - - public static IEnumerable SendMail_MultiLineDomainLiterals_Data() - { - foreach (bool async in new[] { true, false }) - { - foreach (string address in new[] { "foo@[\r\n bar]", "foo@[bar\r\n ]", "foo@[bar\r\n baz]" }) - { - yield return new object[] { address, "foo@example.com", async }; - yield return new object[] { "foo@example.com", address, async }; - } - } - } } } diff --git a/src/libraries/System.Net.Mail/tests/Functional/StartTlsTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTlsTest.cs similarity index 72% rename from src/libraries/System.Net.Mail/tests/Functional/StartTlsTest.cs rename to src/libraries/System.Net.Mail/tests/Functional/SmtpClientTlsTest.cs index b6800c25789f90..3a5ca8f6262c1a 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/StartTlsTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTlsTest.cs @@ -22,7 +22,7 @@ public class CertificateSetup : IDisposable public CertificateSetup() { - (serverCert, serverChain) = System.Net.Test.Common.Configuration.Certificates.GenerateCertificates("localhost", nameof(SmtpClientStartTlsTest<>)); + (serverCert, serverChain) = System.Net.Test.Common.Configuration.Certificates.GenerateCertificates("localhost", nameof(SmtpClientTlsTest<>)); serverCertContext = SslStreamCertificateContext.Create(serverCert, serverChain); } @@ -36,13 +36,13 @@ public void Dispose() } } - public abstract class SmtpClientStartTlsTest : LoopbackServerTestBase + public abstract class SmtpClientTlsTest : LoopbackServerTestBase where TSendMethod : ISendMethodProvider { private CertificateSetup _certificateSetup; private Func? _serverCertValidationCallback; - public SmtpClientStartTlsTest(ITestOutputHelper output, CertificateSetup certificateSetup) : base(output) + public SmtpClientTlsTest(ITestOutputHelper output, CertificateSetup certificateSetup) : base(output) { _certificateSetup = certificateSetup; Server.SslOptions = new SslServerAuthenticationOptions @@ -59,61 +59,57 @@ public SmtpClientStartTlsTest(ITestOutputHelper output, CertificateSetup certifi [Fact] public async Task EnableSslServerSupports_UsesTls() { - using SmtpClient client = Server.CreateClient(); _serverCertValidationCallback = (cert, chain, errors) => { return true; }; - client.Credentials = new NetworkCredential("foo", "bar"); - client.EnableSsl = true; + Smtp.Credentials = new NetworkCredential("foo", "bar"); + Smtp.EnableSsl = true; MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); - await SendMail(client, msg); + await SendMail(msg); Assert.True(Server.IsEncrypted, "TLS was not negotiated."); - Assert.Equal(client.Host, Server.TlsHostName); + Assert.Equal(Smtp.Host, Server.TlsHostName); } [Fact] public async Task EnableSsl_NoServerSupport_NoTls() { - using SmtpClient client = Server.CreateClient(); Server.SslOptions = null; - client.Credentials = new NetworkCredential("foo", "bar"); - client.EnableSsl = true; + Smtp.Credentials = new NetworkCredential("foo", "bar"); + Smtp.EnableSsl = true; MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); - await SendMail(client, msg); + await SendMail(msg); } [Fact] public async Task DisableSslServerSupport_NoTls() { - using SmtpClient client = Server.CreateClient(); - client.Credentials = new NetworkCredential("foo", "bar"); - client.EnableSsl = false; + Smtp.Credentials = new NetworkCredential("foo", "bar"); + Smtp.EnableSsl = false; MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); - await SendMail(client, msg); + await SendMail(msg); Assert.False(Server.IsEncrypted, "TLS was negotiated when it should not have been."); } [Fact] public async Task AuthenticationException_Propagates() { - using SmtpClient client = Server.CreateClient(); _serverCertValidationCallback = (cert, chain, errors) => { return false; // force auth errors }; - client.Credentials = new NetworkCredential("foo", "bar"); - client.EnableSsl = true; + Smtp.Credentials = new NetworkCredential("foo", "bar"); + Smtp.EnableSsl = true; MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); - await SendMail(client, msg); + await SendMail(msg); } [Fact] @@ -128,19 +124,18 @@ public async Task ClientCertificateRequired_Sent() return true; }; - using SmtpClient client = Server.CreateClient(); _serverCertValidationCallback = (cert, chain, errors) => { return true; }; - client.Credentials = new NetworkCredential("foo", "bar"); - client.EnableSsl = true; - client.ClientCertificates.Add(clientCert); + Smtp.Credentials = new NetworkCredential("foo", "bar"); + Smtp.EnableSsl = true; + Smtp.ClientCertificates.Add(clientCert); MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); - await SendMail(client, msg); + await SendMail(msg); Assert.True(Server.IsEncrypted, "TLS was not negotiated."); Assert.Equal(clientCert, receivedClientCert); } @@ -160,20 +155,20 @@ private bool ServerCertValidationCallback(object sender, X509Certificate? certif // since the tests change global state (ServicePointManager.ServerCertificateValidationCallback), we need to run them in isolation [Collection(nameof(DisableParallelization))] - public class StartTlsTest_Send : SmtpClientStartTlsTest, IClassFixture + public class SmtpClientTlsTest_Send : SmtpClientTlsTest, IClassFixture { - public StartTlsTest_Send(ITestOutputHelper output, CertificateSetup certificateSetup) : base(output, certificateSetup) { } + public SmtpClientTlsTest_Send(ITestOutputHelper output, CertificateSetup certificateSetup) : base(output, certificateSetup) { } } [Collection(nameof(DisableParallelization))] - public class StartTlsTest_SendAsync : SmtpClientStartTlsTest, IClassFixture + public class SmtpClientTlsTest_SendAsync : SmtpClientTlsTest, IClassFixture { - public StartTlsTest_SendAsync(ITestOutputHelper output, CertificateSetup certificateSetup) : base(output, certificateSetup) { } + public SmtpClientTlsTest_SendAsync(ITestOutputHelper output, CertificateSetup certificateSetup) : base(output, certificateSetup) { } } [Collection(nameof(DisableParallelization))] - public class StartTlsTest_SendMailAsync : SmtpClientStartTlsTest, IClassFixture + public class SmtpClientTlsTest_SendMailAsync : SmtpClientTlsTest, IClassFixture { - public StartTlsTest_SendMailAsync(ITestOutputHelper output, CertificateSetup certificateSetup) : base(output, certificateSetup) { } + public SmtpClientTlsTest_SendMailAsync(ITestOutputHelper output, CertificateSetup certificateSetup) : base(output, certificateSetup) { } } } \ No newline at end of file diff --git a/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj b/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj index 76f0a6cd2692e9..9306e3b895fcea 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj +++ b/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj @@ -41,7 +41,10 @@ - + + + + From 0b1093e247c4f670c1bbcc05d4b1f080fe0d38d0 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Tue, 15 Apr 2025 15:07:23 +0200 Subject: [PATCH 3/5] Add more tests --- .../Functional/LoopbackServerTestBase.cs | 49 +++-- .../tests/Functional/LoopbackSmtpServer.cs | 197 ++++++++++++++++-- .../Functional/SmtpClientAttachmentTest.cs | 104 +++++++++ .../tests/Functional/SmtpClientAuthTest.cs | 6 - .../Functional/SmtpClientConnectionTest.cs | 59 ++++++ .../Functional/SmtpClientSendMailTest.cs | 161 +++++++++++++- .../SmtpClientSpecifiedPickupDirectoryTest.cs | 2 +- .../tests/Functional/SmtpClientTest.cs | 4 +- .../tests/Functional/SmtpClientTlsTest.cs | 23 +- .../System.Net.Mail.Functional.Tests.csproj | 4 +- 10 files changed, 550 insertions(+), 59 deletions(-) create mode 100644 src/libraries/System.Net.Mail/tests/Functional/SmtpClientAttachmentTest.cs create mode 100644 src/libraries/System.Net.Mail/tests/Functional/SmtpClientConnectionTest.cs diff --git a/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs b/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs index b9571fba916a53..0d00087e61427f 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/LoopbackServerTestBase.cs @@ -7,8 +7,10 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Net.Mail.Tests; +using System.Threading; using System.Threading.Tasks; using Xunit; +using Xunit.Sdk; using Xunit.Abstractions; namespace System.Net.Mail.Tests @@ -62,7 +64,7 @@ public LoopbackServerTestBase(ITestOutputHelper output) Server = new LoopbackSmtpServer(Output); } - private Task SendMailInternal(MailMessage msg) + private Task SendMailInternal(MailMessage msg, CancellationToken cancellationToken, bool? asyncExpectDirectException) { switch (T.SendMethod) { @@ -101,28 +103,45 @@ public LoopbackServerTestBase(ITestOutputHelper output) try { Smtp.SendAsync(msg, tcs); + + if (asyncExpectDirectException == true) + { + Assert.Fail($"No exception thrown"); + } + return tcs.Task; } - catch (Exception ex) + catch (Exception ex) when (ex is not XunitException) { Smtp.SendCompleted -= handler; + + if (asyncExpectDirectException == false) + { + Assert.Fail($"Expected exception via callback, got direct: {ex}"); + } + return Task.FromResult(ex); } case SendMethod.SendMailAsync: try { - return Smtp.SendMailAsync(msg).ContinueWith(t => + Task task = Smtp.SendMailAsync(msg, cancellationToken); + + if (asyncExpectDirectException == true) { - if (t.IsFaulted) - { - return t.Exception?.InnerException; - } - return null; - }); + Assert.Fail($"No exception thrown"); + } + + return task.ContinueWith(t => t.Exception?.InnerException); } - catch (Exception ex) + catch (Exception ex) when (ex is not XunitException) { + if (asyncExpectDirectException == false) + { + Assert.Fail($"Expected stored exception, got direct: {ex}"); + } + return Task.FromResult(ex); } @@ -131,17 +150,17 @@ public LoopbackServerTestBase(ITestOutputHelper output) } } - protected async Task SendMail(MailMessage msg) + protected async Task SendMail(MailMessage msg, CancellationToken cancellationToken = default) { - Exception? ex = await SendMailInternal(msg); + Exception? ex = await SendMailInternal(msg, cancellationToken, null); Assert.Null(ex); } - protected async Task SendMail(MailMessage msg) where TException : Exception + protected async Task SendMail(MailMessage msg, CancellationToken cancellationToken = default, bool unwrapException = true, bool asyncDirectException = false) where TException : Exception { - Exception? ex = await SendMailInternal(msg); + Exception? ex = await SendMailInternal(msg, cancellationToken, asyncDirectException); - if (T.SendMethod != SendMethod.Send && typeof(TException) != typeof(SmtpException)) + if (unwrapException && T.SendMethod != SendMethod.Send && typeof(TException) != typeof(SmtpException)) { ex = Assert.IsType(ex).InnerException; } diff --git a/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs b/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs index ed221f72f20d52..29d9a978890119 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/LoopbackSmtpServer.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Net; using System.Net.Mail; +using System.Net.Mime; using System.Net.Security; using System.Net.Sockets; using System.Text; @@ -40,13 +41,13 @@ public class LoopbackSmtpServer : IDisposable public Action OnConnected; public Action OnHelloReceived; - public Action OnCommandReceived; + public Func OnCommandReceived; public Action OnUnknownCommand; public Action OnQuitReceived; public string ClientDomain { get; private set; } public string MailFrom { get; private set; } - public string MailTo { get; private set; } + public List MailTo { get; private set; } = new List(); public string UsernamePassword { get; private set; } public string Username { get; private set; } public string Password { get; private set; } @@ -87,6 +88,18 @@ private async Task HandleConnectionAsync(Socket socket) var buffer = new byte[1024].AsMemory(); Stream stream = new NetworkStream(socket); + string lastTag = string.Empty; + void LogMessage(string tag, string message) + { + StringReader reader = new(message); + while (reader.ReadLine() is string line) + { + tag = tag == lastTag ? " " : tag; + _output?.WriteLine($"{tag}> {line}"); + lastTag = tag; + } + } + async ValueTask ReceiveMessageAsync(bool isBody = false) { var terminator = isBody ? s_bodyTerminator : s_messageTerminator; @@ -104,7 +117,7 @@ async ValueTask ReceiveMessageAsync(bool isBody = false) MessagesReceived++; string message = Encoding.UTF8.GetString(buffer.Span.Slice(0, received - suffix)); - _output?.WriteLine($"Client> {message}"); + LogMessage("Client", Encoding.UTF8.GetString(buffer.Span.Slice(0, received))); return message; } @@ -114,7 +127,7 @@ async ValueTask SendMessageAsync(string text) bytes.Span[^2] = (byte)'\r'; bytes.Span[^1] = (byte)'\n'; - _output?.WriteLine($"Server> {text}"); + LogMessage("Server", text + "\r\n"); await stream.WriteAsync(bytes); await stream.FlushAsync(); } @@ -133,16 +146,22 @@ async ValueTask SendMessageAsync(string text) if (message.ToLower().StartsWith("helo ") || message.ToLower().StartsWith("ehlo ")) { ClientDomain = message.Substring(5).ToLower(); - OnCommandReceived?.Invoke(message.Substring(0, 4), ClientDomain); + + if (OnCommandReceived?.Invoke(message.Substring(0, 4), ClientDomain) is string reply) + { + await SendMessageAsync(reply); + continue; + } + OnHelloReceived?.Invoke(ClientDomain); await SendMessageAsync("250-localhost, mock server here"); if (SupportSmtpUTF8) await SendMessageAsync("250-SMTPUTF8"); if (SslOptions != null && stream is not SslStream) await SendMessageAsync("250-STARTTLS"); await SendMessageAsync( - "250 AUTH PLAIN LOGIN" + - (AdvertiseNtlmAuthSupport ? " NTLM" : "") + - (AdvertiseGssapiAuthSupport ? " GSSAPI" : "")); + "250 AUTH PLAIN LOGIN" + + (AdvertiseNtlmAuthSupport ? " NTLM" : "") + + (AdvertiseGssapiAuthSupport ? " GSSAPI" : "")); continue; } @@ -151,7 +170,11 @@ await SendMessageAsync( string command = colonIndex == -1 ? message : message.Substring(0, colonIndex); string argument = command.Length == message.Length ? string.Empty : message.Substring(colonIndex + 1).Trim(); - OnCommandReceived?.Invoke(command, argument); + if (OnCommandReceived?.Invoke(command, argument) is string response) + { + await SendMessageAsync(response); + continue; + } if (command.StartsWith("AUTH", StringComparison.OrdinalIgnoreCase)) { @@ -243,11 +266,13 @@ await SendMessageAsync( case "MAIL FROM": MailFrom = argument; + MailTo.Clear(); + Message = null; await SendMessageAsync("250 Ok"); break; case "RCPT TO": - MailTo = argument; + MailTo.Add(argument); await SendMessageAsync("250 Ok"); break; @@ -294,52 +319,180 @@ public void Dispose() } } - public class ParsedMailMessage { public readonly IReadOnlyDictionary Headers; public readonly string Body; + public readonly string RawBody; + public readonly List Attachments; private string GetHeader(string name) => Headers.TryGetValue(name, out string value) ? value : "NOT-PRESENT"; public string From => GetHeader("From"); public string To => GetHeader("To"); + public string Cc => GetHeader("Cc"); public string Subject => GetHeader("Subject"); - private ParsedMailMessage(Dictionary headers, string body) + private ContentType _contentType; + public ContentType ContentType => _contentType ??= new ContentType(GetHeader("Content-Type")); + + private ParsedMailMessage(Dictionary headers, string body, string rawBody, List attachments) { Headers = headers; Body = body; + RawBody = rawBody; + Attachments = attachments; } - public static ParsedMailMessage Parse(string data) + private static (Dictionary headers, string content) ParseContent(ReadOnlySpan data) { Dictionary headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + List attachments = new List(); - ReadOnlySpan dataSpan = data; string body = null; - while (!dataSpan.IsEmpty) + // Parse headers with support for folded lines + string currentHeaderName = null; + StringBuilder currentHeaderValue = null; + + while (!data.IsEmpty) { - int endOfLine = dataSpan.IndexOf('\n'); + int endOfLine = data.IndexOf('\n'); Debug.Assert(endOfLine != -1, "Expected valid \r\n terminated lines"); - var line = dataSpan.Slice(0, endOfLine).TrimEnd('\r'); + var line = data.Slice(0, endOfLine).TrimEnd('\r'); if (line.IsEmpty) { - body = dataSpan.Slice(endOfLine + 1).TrimEnd(stackalloc char[] { '\r', '\n' }).ToString(); + // End of headers section - add the last header if there is one + if (currentHeaderName != null && currentHeaderValue != null) + { + headers.Add(currentHeaderName, currentHeaderValue.ToString().Trim()); + } + + body = data.Slice(endOfLine + 1).TrimEnd(stackalloc char[] { '\r', '\n' }).ToString(); break; } - else + else if ((line[0] == ' ' || line[0] == '\t') && currentHeaderName != null) + { + // This is a folded line, append it to the current header value + currentHeaderValue.Append(' ').Append(line.ToString().TrimStart()); + } + else // new header { + // If we have a header being built, add it now + if (currentHeaderName != null && currentHeaderValue != null) + { + headers.Add(currentHeaderName, currentHeaderValue.ToString().Trim()); + } + + // Start a new header int colon = line.IndexOf(':'); Debug.Assert(colon != -1, "Expected a valid header"); - headers.Add(line.Slice(0, colon).Trim().ToString(), line.Slice(colon + 1).Trim().ToString()); - dataSpan = dataSpan.Slice(endOfLine + 1); + currentHeaderName = line.Slice(0, colon).Trim().ToString(); + currentHeaderValue = new StringBuilder(line.Slice(colon + 1).ToString()); + } + + data = data.Slice(endOfLine + 1); + } + + return (headers, body); + } + + public static ParsedMailMessage Parse(string data) + { + List attachments = new List(); + string rawBody; + (Dictionary headers, string body) = ParseContent(data); + rawBody = body; + + // Check if this is a multipart message + string contentType = headers.TryGetValue("Content-Type", out string ct) ? ct : string.Empty; + if (contentType.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase)) + { + // Extract the boundary + string boundary = ExtractBoundary(contentType); + if (!string.IsNullOrEmpty(boundary)) + { + // Parse multipart body + (attachments, body) = ParseMultipartBody(body, boundary); } } - return new ParsedMailMessage(headers, body); + return new ParsedMailMessage(headers, body, rawBody, attachments); } + + private static string ExtractBoundary(string contentType) + { + int boundaryIndex = contentType.IndexOf("boundary=", StringComparison.OrdinalIgnoreCase); + if (boundaryIndex < 0) + return null; + + string boundaryPart = contentType.Substring(boundaryIndex + 9); + if (boundaryPart.StartsWith("\"")) + { + int endQuote = boundaryPart.IndexOf("\"", 1); + if (endQuote > 0) + return boundaryPart.Substring(1, endQuote - 1); + } + else + { + int endBoundary = boundaryPart.IndexOfAny(new[] { ';', ' ' }); + return endBoundary > 0 ? boundaryPart.Substring(0, endBoundary) : boundaryPart; + } + + return null; + } + + private static (List attachments, string textBody) ParseMultipartBody(string body, string boundary) + { + string textBody = null; + List attachments = new List(); + + string[] parts = body.Split(new[] { "--" + boundary }, StringSplitOptions.None); + + Debug.Assert(string.IsNullOrWhiteSpace(parts[0]), "Expected empty first part"); + Debug.Assert(parts[^1] == "--", "Expected empty last part"); + for (int i = 1; i < parts.Length - 1; i++) + { + string part = parts[i]; + + Debug.Assert(part.StartsWith("\r\n")); + + (Dictionary headers, string content) = ParseContent(part[2..]); + + ContentType contentType = new ContentType(headers["Content-Type"]); + + // Check if this part is an attachment + if (headers.TryGetValue("Content-Disposition", out string disposition) && + disposition.StartsWith("attachment", StringComparison.OrdinalIgnoreCase)) + { + attachments.Add(new ParsedAttachment + { + ContentType = contentType, + RawContent = content, + Headers = headers + }); + } + + // Check if this is a text part + else if (contentType.MediaType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) && + textBody == null) + { + textBody = content; + } + } + + return (attachments, textBody ?? ""); + } + } + + public class ParsedAttachment + { + public ContentType ContentType { get; set; } + public string RawContent { get; set; } + public IDictionary Headers { get; set; } + + private string GetHeader(string name) => Headers.TryGetValue(name, out string value) ? value : "NOT-PRESENT"; + public string ContentTransferEncoding => GetHeader("Content-Transfer-Encoding"); } } } diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAttachmentTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAttachmentTest.cs new file mode 100644 index 00000000000000..a716d63780b6f7 --- /dev/null +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAttachmentTest.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +using System.Net.Mime; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Mail.Tests +{ + public abstract class SmtpClientAttachmentTest : LoopbackServerTestBase where T : ISendMethodProvider + { + public SmtpClientAttachmentTest(ITestOutputHelper output) : base(output) + { + } + + private class ThrowingStream() : Stream + { + + public override bool CanRead => throw new NotImplementedException(); + public override bool CanSeek => throw new NotImplementedException(); + public override bool CanWrite => throw new NotImplementedException(); + public override long Length => throw new NotImplementedException(); + public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override void Flush() => throw new NotImplementedException(); + public override int Read(byte[] buffer, int offset, int count) => throw new InvalidOperationException("Something wrong happened"); + public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); + public override void SetLength(long length) => throw new NotImplementedException(); + public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + } + + [Fact] + public async Task AtachmentStreamThrows_Exception() + { + string attachmentFilename = "test.txt"; + byte[] attachmentContent = Encoding.UTF8.GetBytes("File Contents\r\n"); + + string body = "This is a test mail."; + + using var msg = new MailMessage() + { + From = new MailAddress("foo@example.com"), + To = { new MailAddress("baz@example.com") }, + Attachments = { + new Attachment(new ThrowingStream(), attachmentFilename, MediaTypeNames.Text.Plain) + }, + Subject = "Test Subject", + Body = body + }; + + await SendMail(msg); + } + + [Fact] + public async Task TextFileAttachment() + { + string attachmentFilename = "test.txt"; + byte[] attachmentContent = Encoding.UTF8.GetBytes("File Contents\r\n"); + + string body = "This is a test mail."; + + using var msg = new MailMessage() + { + From = new MailAddress("foo@example.com"), + To = { new MailAddress("baz@example.com") }, + Attachments = { + new Attachment(new MemoryStream(attachmentContent), attachmentFilename, MediaTypeNames.Text.Plain) + }, + Subject = "Test Subject", + Body = body + }; + + await SendMail(msg); + + Assert.Equal(body, Server.Message.Body); + Assert.Collection(Server.Message.Attachments, + attachment => + { + Assert.Equal(attachmentFilename, attachment.ContentType.Name); + Assert.Equal(MediaTypeNames.Text.Plain, attachment.ContentType.MediaType); + Assert.Equal("base64", attachment.ContentTransferEncoding); + Assert.Equal(attachmentContent, Convert.FromBase64String(attachment.RawContent)); + }); + } + } + + public class SmtpClientAttachmentTest_Send : SmtpClientAttachmentTest + { + public SmtpClientAttachmentTest_Send(ITestOutputHelper output) : base(output) { } + } + + public class SmtpClientAttachmentTest_SendAsync : SmtpClientAttachmentTest + { + public SmtpClientAttachmentTest_SendAsync(ITestOutputHelper output) : base(output) { } + } + + public class SmtpClientAttachmentTest_SendMailAsync : SmtpClientAttachmentTest + { + public SmtpClientAttachmentTest_SendMailAsync(ITestOutputHelper output) : base(output) { } + } +} diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAuthTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAuthTest.cs index e56362ff3b2984..ca0805958b8dc9 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAuthTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAuthTest.cs @@ -1,11 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.NetworkInformation; -using System.Net.Security; -using System.Security.Authentication; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; using System.Net.Mail.Tests; using System.Net.Test.Common; using System.Threading.Tasks; @@ -56,7 +51,6 @@ public async Task TestGssapiAuthentication() Assert.Equal("GSSAPI", Server.AuthMethodUsed, StringComparer.OrdinalIgnoreCase); } - } public class SmtpClientAuthTest_Send : SmtpClientAuthTest diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientConnectionTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientConnectionTest.cs new file mode 100644 index 00000000000000..4ae8ccf4d692c8 --- /dev/null +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientConnectionTest.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Mail.Tests; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.Mail.Tests +{ + public abstract class SmtpClientConnectionTest : LoopbackServerTestBase + where TSendMethod : ISendMethodProvider + { + public SmtpClientConnectionTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public async Task SocketClosed() + { + Server.OnConnected = socket => socket.Close(); + await SendMail(new MailMessage("mono@novell.com", "everyone@novell.com", "introduction", "hello")); + } + + [Fact] + public async Task EHelloNotRecognized_RestartWithHello() + { + bool helloReceived = false; + Server.OnCommandReceived = (command, arg) => + { + helloReceived |= string.Equals(command, "HELO", StringComparison.OrdinalIgnoreCase); + if (string.Equals(command, "EHLO", StringComparison.OrdinalIgnoreCase)) + { + return "502 Not implemented"; + } + + return null; + }; + + await SendMail(new MailMessage("mono@novell.com", "everyone@novell.com", "introduction", "hello")); + Assert.True(helloReceived, "HELO command was not received."); + } + } + + public class SmtpClientConnectionTest_Send : SmtpClientConnectionTest + { + public SmtpClientConnectionTest_Send(ITestOutputHelper output) : base(output) { } + } + + public class SmtpClientConnectionTest_SendAsync : SmtpClientConnectionTest + { + public SmtpClientConnectionTest_SendAsync(ITestOutputHelper output) : base(output) { } + } + + public class SmtpClientConnectionTest_SendMailAsync : SmtpClientConnectionTest + { + public SmtpClientConnectionTest_SendMailAsync(ITestOutputHelper output) : base(output) { } + } +} diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs index 6309a216328813..a65a4d63d05dcb 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs @@ -1,13 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.NetworkInformation; -using System.Net.Security; -using System.Security.Authentication; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; using System.Net.Mail.Tests; -using System.IO; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -25,14 +19,14 @@ public SmtpClientSendMailTest(ITestOutputHelper output) : base(output) [Fact] public async Task Message_Null() { - await SendMail(null); + await SendMail(null, asyncDirectException: true); } [Fact] public async Task Network_Host_Whitespace() { Smtp.Host = " \r\n "; - await SendMail(new MailMessage("mono@novell.com", "everyone@novell.com", "introduction", "hello")); + await SendMail(new MailMessage("mono@novell.com", "everyone@novell.com", "introduction", "hello"), asyncDirectException: true); } [Fact] @@ -53,10 +47,10 @@ public async Task MailDelivery(string body) Smtp.Credentials = new NetworkCredential("foo", "bar"); MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", body); - await SendMail(msg).WaitAsync(TimeSpan.FromSeconds(30)); + await SendMail(msg); Assert.Equal("", Server.MailFrom); - Assert.Equal("", Server.MailTo); + Assert.Equal("", Assert.Single(Server.MailTo)); Assert.Equal("hello", Server.Message.Subject); Assert.Equal(body ?? "", Server.Message.Body); Assert.Equal(GetClientDomain(), Server.ClientDomain); @@ -141,7 +135,7 @@ public async Task TestMultipleMailDelivery() await SendMail(msg); Assert.Equal("", Server.MailFrom); - Assert.Equal("", Server.MailTo); + Assert.Equal("", Assert.Single(Server.MailTo)); Assert.Equal("hello", Server.Message.Subject); Assert.Equal("howdydoo", Server.Message.Body); Assert.Equal(GetClientDomain(), Server.ClientDomain); @@ -170,6 +164,151 @@ public static IEnumerable SendMail_MultiLineDomainLiterals_Data() yield return new object[] { "foo@example.com", address }; } } + + [Fact] + public async Task MultipleRecipients_Success() + { + using var msg = new MailMessage() + { + From = new MailAddress("foo@example.com"), + To = { + new MailAddress("bar@example.com"), + new MailAddress("baz@example.com") + }, + CC = { + new MailAddress("cc1@example.com"), + new MailAddress("cc2@example.com"), + }, + Subject = "subject", + Body = "body" + }; + await SendMail(msg); + + Assert.Equal("", Server.MailFrom); + Assert.Equal(["", "", "", ""], Server.MailTo); + Assert.Equal("subject", Server.Message.Subject); + Assert.Equal("body", Server.Message.Body); + Assert.Equal("bar@example.com, baz@example.com", Server.Message.To); + Assert.Equal("cc1@example.com, cc2@example.com", Server.Message.Cc); + } + + [Fact] + public async Task MultipleRecipients_Failure_One() + { + Server.OnCommandReceived = (command, argument) => + { + if (string.Equals("RCPT TO", command, StringComparison.OrdinalIgnoreCase) && argument.Contains("bar")) + { + return "550 unknown recipient"; + } + + return null; + }; + + using var msg = new MailMessage() + { + From = new MailAddress("foo@example.com"), + To = { + new MailAddress("bar@example.com"), + new MailAddress("baz@example.com") + }, + CC = { + new MailAddress("cc1@example.com"), + new MailAddress("cc2@example.com"), + }, + Subject = "subject", + Body = "body" + }; + + var ex = await SendMail(msg, unwrapException: false); + Assert.Equal("", ex.FailedRecipient); + + // still expect the message to be sent since other recipients were available + Assert.Equal("body", Server.Message.Body); + Assert.Equal("bar@example.com, baz@example.com", Server.Message.To); + Assert.Equal("cc1@example.com, cc2@example.com", Server.Message.Cc); + } + + [Fact] + public async Task MultipleRecipients_Failure_Many() + { + Server.OnCommandReceived = (command, argument) => + { + if (string.Equals("RCPT TO", command, StringComparison.OrdinalIgnoreCase) && !argument.Contains("bar")) + { + return "550 unknown recipient"; + } + + return null; + }; + + using var msg = new MailMessage() + { + From = new MailAddress("foo@example.com"), + To = { + new MailAddress("bar@example.com"), + new MailAddress("baz@example.com") + }, + CC = { + new MailAddress("cc1@example.com"), + new MailAddress("cc2@example.com"), + }, + Subject = "subject", + Body = "body" + }; + + var ex = await SendMail(msg, unwrapException: false); + Assert.Collection(ex.InnerExceptions, + e => { Assert.Equal("", e.FailedRecipient); }, + e => { Assert.Equal("", e.FailedRecipient); }, + e => { Assert.Equal("", e.FailedRecipient); } + ); + + // still expect the message to be sent since other recipients were available + Assert.Equal("body", Server.Message.Body); + Assert.Equal("bar@example.com, baz@example.com", Server.Message.To); + Assert.Equal("cc1@example.com, cc2@example.com", Server.Message.Cc); + } + + [Fact] + public async Task MultipleRecipients_Failure_All() + { + Server.OnCommandReceived = (command, argument) => + { + if (string.Equals("RCPT TO", command, StringComparison.OrdinalIgnoreCase)) + { + return "550 unknown recipient"; + } + + return null; + }; + + using var msg = new MailMessage() + { + From = new MailAddress("foo@example.com"), + To = { + new MailAddress("bar@example.com"), + new MailAddress("baz@example.com") + }, + CC = { + new MailAddress("cc1@example.com"), + new MailAddress("cc2@example.com"), + }, + Subject = "subject", + Body = "body" + }; + + var ex = await SendMail(msg, unwrapException: false); + Assert.Collection(ex.InnerExceptions, + e => { Assert.Equal("", e.FailedRecipient); }, + e => { Assert.Equal("", e.FailedRecipient); }, + e => { Assert.Equal("", e.FailedRecipient); }, + e => { Assert.Equal("", e.FailedRecipient); } + ); + + // No recipients succeeded, nothing to send + Assert.Null(Server.Message); + } } public class SmtpClientSendMailTest_Send : SmtpClientSendMailTest diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSpecifiedPickupDirectoryTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSpecifiedPickupDirectoryTest.cs index 92021692d8eb16..595cb2eecdf0ef 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSpecifiedPickupDirectoryTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSpecifiedPickupDirectoryTest.cs @@ -86,7 +86,7 @@ public async Task Send_SpecifiedPickupDirectoryInvalid(string location) { Smtp.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory; Smtp.PickupDirectoryLocation = location; - await SendMail(new MailMessage("mono@novell.com", "everyone@novell.com", "introduction", "hello")); + await SendMail(new MailMessage("mono@novell.com", "everyone@novell.com", "introduction", "hello"), asyncDirectException: true); } } diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs index e69a9db6a5ed27..332b14b3baed78 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs @@ -245,7 +245,7 @@ public void TestMailDelivery() client.Send(msg); Assert.Equal("", server.MailFrom); - Assert.Equal("", server.MailTo); + Assert.Equal("", Assert.Single(server.MailTo)); Assert.Equal("hello", server.Message.Subject); Assert.Equal("howdydoo", server.Message.Body); Assert.Equal(GetClientDomain(), server.ClientDomain); @@ -324,7 +324,7 @@ public async Task SendMailAsync_CanBeCanceled_CancellationToken() await Task.Run(() => client.SendMailAsync(message)).WaitAsync(TestHelper.PassingTestTimeout); Assert.Equal("", server.MailFrom); - Assert.Equal("", server.MailTo); + Assert.Equal("", Assert.Single(server.MailTo)); Assert.Equal("Foo", server.Message.Subject); Assert.Equal("Bar", server.Message.Body); Assert.Equal(GetClientDomain(), server.ClientDomain); diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTlsTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTlsTest.cs index 3a5ca8f6262c1a..1d835acb4f4305 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTlsTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientTlsTest.cs @@ -57,7 +57,7 @@ public SmtpClientTlsTest(ITestOutputHelper output, CertificateSetup certificateS } [Fact] - public async Task EnableSslServerSupports_UsesTls() + public async Task EnableSsl_ServerSupports_UsesTls() { _serverCertValidationCallback = (cert, chain, errors) => { @@ -73,6 +73,27 @@ public async Task EnableSslServerSupports_UsesTls() Assert.Equal(Smtp.Host, Server.TlsHostName); } + [Theory] + [InlineData("500 T'was just a jest.")] + [InlineData("300 I don't know what I am doing.")] + [InlineData("I don't know what I am doing.")] + public async Task EnableSsl_ServerError(string reply) + { + Smtp.EnableSsl = true; + MailMessage msg = new MailMessage("foo@example.com", "bar@example.com", "hello", "howdydoo"); + + Server.OnCommandReceived = (command, parameter) => + { + if (string.Equals(command, "STARTTLS", StringComparison.OrdinalIgnoreCase)) + return reply; + + return null; + }; + + await SendMail(msg); + } + + [Fact] public async Task EnableSsl_NoServerSupport_NoTls() { diff --git a/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj b/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj index 9306e3b895fcea..f75c2e37169ee7 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj +++ b/src/libraries/System.Net.Mail/tests/Functional/System.Net.Mail.Functional.Tests.csproj @@ -40,10 +40,12 @@ + + + - From adfaa95b76d132a23f9101d848617219b051cd5e Mon Sep 17 00:00:00 2001 From: Radek Zikmund <32671551+rzikm@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:33:40 +0200 Subject: [PATCH 4/5] Update src/libraries/System.Net.Mail/tests/Functional/SmtpClientAttachmentTest.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../tests/Functional/SmtpClientAttachmentTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAttachmentTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAttachmentTest.cs index a716d63780b6f7..a18ff95dc82c0a 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAttachmentTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientAttachmentTest.cs @@ -17,7 +17,7 @@ public SmtpClientAttachmentTest(ITestOutputHelper output) : base(output) { } - private class ThrowingStream() : Stream + private class ThrowingStream : Stream { public override bool CanRead => throw new NotImplementedException(); From f4d212100db59c8c73cd85cdbc50c50cca198299 Mon Sep 17 00:00:00 2001 From: Radek Zikmund Date: Thu, 17 Apr 2025 14:43:48 +0200 Subject: [PATCH 5/5] Fix nondeterministic test --- .../tests/Functional/MailMessageTest.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs b/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs index dc871a4d6846b1..c73daa15422856 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/MailMessageTest.cs @@ -192,7 +192,26 @@ blah blah string sent = DecodeSentMailMessage(messageWithSubjectAndBody).Raw; sent = Regex.Replace(sent, "Date:.*?\r\n", "Date: DATE\r\n"); - sent = Regex.Replace(sent, @"_.{8}-.{4}-.{4}-.{4}-.{12}", "_GUID"); + + // Find outer boundary (in the main Content-Type) + var outerBoundaryMatch = Regex.Match(sent, @"Content-Type: multipart/mixed;\s+boundary=(--boundary_\d+_[a-f0-9-]+)"); + // Find inner boundary (in the nested Content-Type) + var innerBoundaryMatch = Regex.Match(sent, @"Content-Type: multipart/alternative;\s+boundary=(--boundary_\d+_[a-f0-9-]+)"); + + if (outerBoundaryMatch.Success && innerBoundaryMatch.Success) + { + string outerBoundary = outerBoundaryMatch.Groups[1].Value; + string innerBoundary = innerBoundaryMatch.Groups[1].Value; + + // Replace all occurrences of these boundaries + sent = sent.Replace(outerBoundary, "--boundary_1_GUID"); + sent = sent.Replace(innerBoundary, "--boundary_0_GUID"); + } + else + { + // unify boundary GUIDs + sent = Regex.Replace(sent, @"--boundary_\d+_[a-f0-9-]+", "--boundary_?_GUID"); + } // name and charset can appear in different order Assert.Contains("; name=AttachmentName", sent);